# =============================================================================
# Nazev:   create_lab_results_report_v1.0.py
# Verze:   1.0
# Datum:   2026-06-09
# Popis:   Samostatny skript — pekny Excel report laboratornich vysledku ze
#          studii Covance/Labcorp (36940 UCO3001 + 35472 MDD3003).
#          Zdroj dat: MongoDB db "covance", kolekce "results"
#          (standard + microbiology). Vystupni .xlsx ve stylu hlavniho
#          create_report_v2.0.py (modre hlavicky, ramecky, freeze, autofiltr,
#          skutecne Excel datumy DD-MMM-YYYY, hyperlinky mezi listy).
#
#          Listy:
#            1. PREHLED          — KPI dashboard, kazde cislo je odkaz do detailu
#            2. SOUHRN CENTER    — rollup per (studie, centrum, pacient)
#            3. DILI Hyuv zakon  — verdikt + vsech 5 DILI kriterii + jaterni testy
#            4. Abnormality      — pracovni seznam vsech vysledku mimo rozmezi
#            5. Matice pokryti   — pacient x navsteva (kde chybi / je abnormalni)
#            6. Zrusene testy    — vysledky s Cancellation Reason
#            7. Mikrobiologie    — kultivace, auto-flag prvni pozitivni
#            8. Datove mezery    — DILI gap hodnoty + prazdne klicove panely
#            9. Zdroj            — plochy vypis vsech standardnich vysledku (kotva)
#           10. LEGENDA          — vysvetleni flagu, barev, Hyova zakona, studii
#
#          Pozn.: pri spojeni do hlavniho create_report.py staci prevzit sekce
#          load + helpers + write_* a sdilet styly.
# =============================================================================

import re
from datetime import datetime
from collections import defaultdict
from pymongo import MongoClient
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
from openpyxl.utils import get_column_letter

# -- Konfigurace --------------------------------------------------------------
MONGO_URI = "mongodb://192.168.1.76:27017"
OUT_DIR   = "U:/Dropbox/!!!Days/Downloads Z230/"

STUDY_LABEL = {"36940": "UCO3001", "35472": "MDD3003"}
PROTOCOL    = {"36940": "77242113UCO3001", "35472": "42847922MDD3003"}

DILI_DANGER = {"ALT>/=3XULN", "AST>/=3XULN",
               "ALT>/=3XULN & TBIL>/=2XULN", "AST>/=3XULN & TBIL>/=2XULN"}  # ALP NE
DILI_GROUPS = DILI_DANGER | {"ALP<2XULN"}
GAP_VALUES  = {"required testing unavailable", "unable to calc due to loq"}
KEY_PANELS  = {"CHEMISTRY PANEL", "HEMATOLOGY&DIFFERENTIAL PANEL",
               "LIPID PANEL", "URINE MICRO PANEL", "URINE MACRO PANEL"}
LIVER_TESTS    = {"ALT (SGPT)", "AST (SGOT)", "Total Bilirubin", "Alkaline Phosphatase"}
CYTOPENIA_TESTS = {"Hemoglobin", "Platelets", "Neutrophils", "WBC"}

SEV_LABEL = {3: "Kriticka", 2: "Vysoka", 1: "Mirna", 0: ""}

# Poradi navstev pro matici pokryti (chronologicky dle protokolu).
VISIT_ORDER = [
    "Screening", "Induction Week 0", "Induction Week 2", "Induction Week 4",
    "Induction Week 12", "Part 1 Screening", "Part 1 DB Baseline",
    "Part 2 Screening", "Part 2 DB Baseline", "Part 2 DB Week 13",
    "Part 2 DB Every 12 Weeks", "Part 2 OL Induction Baseline",
    "Part 2 OL Induction Day 29", "End of Phase/Treatment", "Early Withdrawal",
    "Retest", "Biopsy", "TB Testing", "Stool Pathogens", "Unscheduled",
]

def visit_group(v):
    if v in ("Screening",):                       return "SCREENING"
    if v.startswith("Induction"):                 return "INDUKCE"
    if v.startswith("Part 1"):                     return "PART 1"
    if v.startswith("Part 2"):                     return "PART 2"
    if v in ("End of Phase/Treatment", "Early Withdrawal", "Retest"): return "UKONCENI"
    return "OSTATNI"

# -- Styly (house style z create_report_v2.0.py) ------------------------------
thin        = Side(style="thin")
border      = Border(left=thin, right=thin, top=thin, bottom=thin)
header_fill = PatternFill("solid", fgColor="4472C4")
header_font = Font(name="Calibri", bold=True, size=11, color="FFFFFF")
data_font   = Font(name="Calibri", size=11)
title_fill  = PatternFill("solid", fgColor="203764")
title_font  = Font(name="Calibri", bold=True, size=14, color="FFFFFF")
problem_fill = PatternFill("solid", fgColor="FFE7E7")
attn_fill    = PatternFill("solid", fgColor="FFF2CC")
ok_fill      = PatternFill("solid", fgColor="E2EFDA")
total_fill   = PatternFill("solid", fgColor="D9E1F2")
red_font     = Font(name="Calibri", size=11, color="C00000")
red_bold     = Font(name="Calibri", bold=True, size=11, color="C00000")
muted_font   = Font(name="Calibri", size=11, color="BFBFBF")
link_font    = Font(name="Calibri", size=11, color="0563C1", underline="single")
bold_font    = Font(name="Calibri", bold=True, size=11)
white_bold   = Font(name="Calibri", bold=True, size=12, color="FFFFFF")

CENTER = Alignment(horizontal="center", vertical="center")
CENTERW = Alignment(horizontal="center", vertical="center", wrap_text=True)
LEFT   = Alignment(horizontal="left", vertical="center")

DATE_FMT = "DD-MMM-YYYY"


# -- Pomocne funkce -----------------------------------------------------------
def parse_flags(flags):
    """Vrati (smer, kvalifikatory). smer in {'H','L',None}."""
    if not flags:
        return None, set()
    direction, quals = None, set()
    for t in str(flags).split():
        if t[:1] == "H":
            direction = direction or "H"
        elif t[:1] == "L":
            direction = direction or "L"
        if t in ("HT", "LT"):
            quals.add("T")
        elif t == "SF":
            quals.add("SF")
        elif t == "RX":
            quals.add("RX")
    return direction, quals


def parse_collected(s):
    if not s:
        return None
    for fmt in ("%b %d, %Y %I:%M %p", "%b %d, %Y"):
        try:
            return datetime.strptime(str(s).strip(), fmt)
        except ValueError:
            pass
    return None


def parse_value_num(v):
    if v is None:
        return None
    v = str(v).strip()
    return float(v) if re.fullmatch(r"-?\d+(\.\d+)?", v) else None


def parse_range(r):
    if not r:
        return (None, None)
    m = re.match(r"^\s*(-?[\d.]+)\s*-\s*(-?[\d.]+)", str(r))
    return (float(m.group(1)), float(m.group(2))) if m else (None, None)


def out_by_pct(value_num, lo, hi, direction):
    if value_num is None or lo is None or hi is None:
        return None
    width = hi - lo
    if width == 0:
        bound = hi if direction == "H" else lo
        return abs(value_num - bound) / bound * 100 if bound else None
    if direction == "H":
        return (value_num - hi) / width * 100
    if direction == "L":
        return (lo - value_num) / width * 100
    return None


def severity(direction, quals, opct, test):
    if direction is None:
        return 0
    if ("T" in quals
            or (opct is not None and opct >= 100)
            or (test in LIVER_TESTS and direction == "H")
            or (test in CYTOPENIA_TESTS and direction == "L")):
        return 3
    if opct is not None and opct >= 50:
        return 2
    return 1


def is_criteria_met(value):
    return value is not None and "criteria met" in str(value).lower()


def rev_flags(fields):
    s = set()
    if fields.get("Subject Data Revision") == "R":
        s.add("S")
    if fields.get("Visit Data Revision") == "R":
        s.add("V")
    if fields.get("Result Data Revision") == "R":
        s.add("R")
    return s


NEG_RE = re.compile(r"(?i)(no enteric pathogens|negative|no growth|no\b.*pathogen)")

def micro_positive(fields):
    if fields.get("Organism") or fields.get("Growth"):
        return True
    tr = fields.get("Test Result")
    return bool(tr and not NEG_RE.search(str(tr)))


# -- Nacteni dat --------------------------------------------------------------
print("Nacitam data z MongoDB...")
client = MongoClient(MONGO_URI)
col = client["covance"]["results"]

raw = list(col.find())
print(f"  results: {len(raw)} dokumentu")

# Predpocet odvozenych poli pro standardni vysledky.
S = []   # standard
M = []   # microbiology
for doc in raw:
    f = doc.get("fields", {})
    rtype = doc.get("resultType")
    base = {
        "id":      str(doc.get("_id")),
        "study":   doc.get("study"),
        "studyl":  STUDY_LABEL.get(doc.get("study"), doc.get("study")),
        "site":    doc.get("site"),
        "subject": doc.get("subject"),
        "gender":  f.get("Gender"),
        "accession": f.get("Accession"),
        "cancel":  f.get("Cancellation Reason"),
        "comments": f.get("Comments"),
        "rev":     rev_flags(f),
        "f":       f,
    }
    if rtype == "microbiology":
        base["visit"]     = f.get("Visit Description")
        base["collected"] = parse_collected(f.get("Collected Time"))
        base["positive"]  = micro_positive(f)
        M.append(base)
    else:
        tg   = f.get("Test Group")
        test = f.get("Test")
        val  = f.get("Value")
        flags = f.get("Flags")
        direction, quals = parse_flags(flags)
        vnum = parse_value_num(val)
        lo, hi = parse_range(f.get("Range"))
        opct = out_by_pct(vnum, lo, hi, direction)
        base.update({
            "visit":     f.get("Visit"),
            "collected": parse_collected(f.get("Collected Time")),
            "tg":        tg,
            "test":      test,
            "value":     val,
            "vnum":      vnum,
            "units":     f.get("Units"),
            "range":     f.get("Range"),
            "flags":     flags,
            "direction": direction,
            "quals":     quals,
            "opct":      opct,
            "sev":       severity(direction, quals, opct, test),
            "is_dili":   tg in DILI_GROUPS,
            "is_gap":    (str(val).strip().lower() in GAP_VALUES if val else False)
                          or (not val and tg in KEY_PANELS and not f.get("Cancellation Reason")),
        })
        S.append(base)

print(f"  standardnich: {len(S)}, mikrobiologie: {len(M)}")

MINDT = datetime.min
def skey(d):
    return (d["study"] or "", d["site"] or "", d["subject"] or "",
            d["collected"] or MINDT, d["accession"] or "",
            d.get("tg") or "", d.get("test") or "")

S.sort(key=skey)

# -- Agregace pro KPI ---------------------------------------------------------
all_docs   = S + M
sites      = sorted({d["site"] for d in all_docs if d["site"]})
subjects   = sorted({d["subject"] for d in all_docs if d["subject"]})
abn_rows   = [d for d in S if d["direction"]]
crit_rows  = [d for d in S if d["sev"] == 3]
cancel_std = [d for d in S if d["cancel"]]
cancel_mic = [d for d in M if d["cancel"]]
gap_rows   = [d for d in S if d["is_gap"]]
rev_rows   = [d for d in S if d["rev"]]
micro_pos  = [d for d in M if d["positive"]]

collected_all = [d["collected"] for d in all_docs if d["collected"]]
dt_min = min(collected_all) if collected_all else None
dt_max = max(collected_all) if collected_all else None

# Hyuv zakon — pozitivni subjekty (konjunkce transaminaza & TBIL kombo).
dili_by_subject = defaultdict(list)
for d in S:
    if d["is_dili"]:
        dili_by_subject[(d["study"], d["site"], d["subject"])].append(d)

def hys_positive(rows):
    met = {d["tg"] for d in rows if is_criteria_met(d["value"])}
    transaminase = ("ALT>/=3XULN" in met) or ("AST>/=3XULN" in met)
    tbil_combo   = ("ALT>/=3XULN & TBIL>/=2XULN" in met) or ("AST>/=3XULN & TBIL>/=2XULN" in met)
    return transaminase and tbil_combo

hys_subjects = [k for k, rows in dili_by_subject.items() if hys_positive(rows)]


# -- Workbook + listy (poradi zalozek) ---------------------------------------
wb = Workbook()
wb.remove(wb.active)
ws_over   = wb.create_sheet("PREHLED")
ws_souhrn = wb.create_sheet("SOUHRN CENTER")
ws_dili   = wb.create_sheet("DILI Hyuv zakon")
ws_abn    = wb.create_sheet("Abnormality")
ws_matice = wb.create_sheet("Matice pokryti")
ws_cancel = wb.create_sheet("Zrusene testy")
ws_micro  = wb.create_sheet("Mikrobiologie")
ws_gaps   = wb.create_sheet("Datove mezery")
ws_zdroj  = wb.create_sheet("Zdroj")
ws_legend = wb.create_sheet("LEGENDA")

# Nazvy listu pro hyperlinky.
N_OVER, N_SOUHRN, N_DILI, N_ABN = "PREHLED", "SOUHRN CENTER", "DILI Hyuv zakon", "Abnormality"
N_MAT, N_CANCEL, N_MICRO, N_GAP, N_ZDROJ = ("Matice pokryti", "Zrusene testy",
                                            "Mikrobiologie", "Datove mezery", "Zdroj")

def link(target_sheet, row, col="A"):
    return f"#'{target_sheet}'!{col}{row}"


# -- Pomocne pro psani --------------------------------------------------------
def write_header(ws, row, headers, widths=None):
    for ci, h in enumerate(headers, 1):
        c = ws.cell(row=row, column=ci, value=h)
        c.font = header_font
        c.fill = header_fill
        c.border = border
        c.alignment = CENTERW
        if widths:
            ws.column_dimensions[get_column_letter(ci)].width = widths[ci - 1]
    ws.row_dimensions[row].height = 30


def cell(ws, r, c, value, font=data_font, fill=None, align=CENTER,
         numfmt=None, hyperlink=None):
    x = ws.cell(row=r, column=c, value=value)
    x.font = font
    x.border = border
    x.alignment = align
    if fill:
        x.fill = fill
    if numfmt:
        x.number_format = numfmt
    if hyperlink:
        x.hyperlink = hyperlink
        x.font = link_font
    return x


# =============================================================================
# PREDVYPOCET ROZLOZENI RADKU (aby hyperlinky znaly cilove radky predem)
# =============================================================================
# Zdroj: kazdy standard doc -> radek (header row 1, data od 2).
zdroj_row = {d["id"]: i + 2 for i, d in enumerate(S)}
# Prvni radek Zdroje pro (subject, visit) — pro matici.
zdroj_first_sv = {}
for d in S:
    k = (d["subject"], d["visit"])
    if k not in zdroj_first_sv:
        zdroj_first_sv[k] = zdroj_row[d["id"]]

# Abnormality: serazeno worst-first.
abn_sorted = sorted(
    abn_rows,
    key=lambda d: (-d["sev"], -(d["opct"] if d["opct"] is not None else -1),
                   d["study"] or "", d["site"] or "", d["subject"] or "",
                   d["collected"] or MINDT, d["test"] or ""),
)
abn_row = {d["id"]: i + 2 for i, d in enumerate(abn_sorted)}
abn_first_subject = {}
for d in abn_sorted:
    k = (d["study"], d["site"], d["subject"])
    if k not in abn_first_subject:
        abn_first_subject[k] = abn_row[d["id"]]

# SOUHRN CENTER: rollup radky (per studie/centrum/pacient) + subtotaly + celkem.
agg = defaultdict(lambda: {"n": 0, "abn": 0, "crit": 0, "gap": 0,
                           "cancel": 0, "rev": 0, "dmin": None, "dmax": None})
for d in S:
    a = agg[(d["study"], d["site"], d["subject"])]
    a["n"] += 1
    if d["direction"]:
        a["abn"] += 1
    if d["sev"] == 3:
        a["crit"] += 1
    if d["is_gap"]:
        a["gap"] += 1
    if d["cancel"]:
        a["cancel"] += 1
    if d["rev"]:
        a["rev"] += 1
    if d["collected"]:
        a["dmin"] = d["collected"] if a["dmin"] is None else min(a["dmin"], d["collected"])
        a["dmax"] = d["collected"] if a["dmax"] is None else max(a["dmax"], d["collected"])

souhrn_keys = sorted(agg.keys(), key=lambda k: (k[0] or "", k[1] or "", k[2] or ""))
# Predpocet radku v SOUHRN: header row1, pak data se subtotaly per centrum.
souhrn_row_by_ssp = {}
r = 2
prev_site = None
for k in souhrn_keys:
    if prev_site is not None and (k[0], k[1]) != prev_site:
        r += 1  # subtotal radek
    souhrn_row_by_ssp[k] = r
    prev_site = (k[0], k[1])
    r += 1


# =============================================================================
# LIST 9: Zdroj  (psano prvni interne — kotva pro vse)
# =============================================================================
zdroj_cols = [
    ("Studie", 12), ("Centrum", 12), ("Pacient", 16), ("Pohlavi", 10),
    ("Navsteva", 22), ("Datum odberu", 15), ("Accession", 14),
    ("Panel", 26), ("Test", 24), ("Hodnota", 14), ("Priznak", 10),
    ("Rozmezi", 14), ("Jednotky", 12), ("Duvod zruseni", 22),
    ("Komentar", 24), ("Revize", 10),
]
write_header(ws_zdroj, 1, [h for h, _ in zdroj_cols], [w for _, w in zdroj_cols])
ws_zdroj.freeze_panes = "A2"
ws_zdroj.auto_filter.ref = f"A1:{get_column_letter(len(zdroj_cols))}1"
for i, d in enumerate(S, 2):
    cell(ws_zdroj, i, 1, d["studyl"])
    cell(ws_zdroj, i, 2, d["site"])
    cell(ws_zdroj, i, 3, d["subject"])
    cell(ws_zdroj, i, 4, d["gender"])
    cell(ws_zdroj, i, 5, d["visit"], align=LEFT)
    cell(ws_zdroj, i, 6, d["collected"], numfmt=DATE_FMT)
    cell(ws_zdroj, i, 7, d["accession"])
    cell(ws_zdroj, i, 8, d["tg"], align=LEFT)
    cell(ws_zdroj, i, 9, d["test"], align=LEFT)
    cell(ws_zdroj, i, 10, d["vnum"] if d["vnum"] is not None else d["value"])
    fc = cell(ws_zdroj, i, 11, d["flags"])
    if d["direction"]:
        fc.fill = attn_fill
    cell(ws_zdroj, i, 12, d["range"])
    cell(ws_zdroj, i, 13, d["units"])
    cell(ws_zdroj, i, 14, d["cancel"], align=LEFT)
    cell(ws_zdroj, i, 15, d["comments"], align=LEFT)
    cell(ws_zdroj, i, 16, ",".join(sorted(d["rev"])) or None)


# =============================================================================
# LIST 1: PREHLED  (KPI dashboard)
# =============================================================================
ws = ws_over
ws.merge_cells("A1:H1")
c = ws.cell(row=1, column=1, value="Laboratorni vysledky — Covance / Labcorp")
c.fill = title_fill
c.font = title_font
c.alignment = LEFT
ws.merge_cells("A2:H2")
fresh = ""
if dt_min and dt_max:
    fresh = f"   |   sber: {dt_min.strftime('%d-%b-%Y')} – {dt_max.strftime('%d-%b-%Y')}"
c2 = ws.cell(row=2, column=1,
             value=f"Vygenerovano {datetime.now().strftime('%d-%b-%Y %H:%M')}{fresh}")
c2.fill = title_fill
c2.font = Font(name="Calibri", size=11, color="FFFFFF")
c2.alignment = LEFT
ws.row_dimensions[1].height = 26
ws.row_dimensions[2].height = 18
for col, w in (("A", 40), ("B", 16), ("C", 28)):
    ws.column_dimensions[col].width = w

n_hys, n_mic = len(hys_subjects), len(micro_pos)
tiles = [
    ("Celkem vysledku", len(all_docs), total_fill, link(N_ZDROJ, 2), None),
    ("Studie", len({d["study"] for d in all_docs}), total_fill, link(N_SOUHRN, 2), None),
    ("Centra", len(sites), total_fill, link(N_SOUHRN, 2), None),
    ("Pacienti", len(subjects), total_fill, link(N_SOUHRN, 2), None),
    ("Abnormalni (mimo rozmezi)", len(abn_rows), attn_fill, link(N_ABN, 2), None),
    ("z toho kriticke", len(crit_rows), attn_fill, link(N_ABN, 2), None),
    ("Bezpecnostni signaly (Hyuv zakon)", n_hys,
     ok_fill if n_hys == 0 else problem_fill, link(N_DILI, 3),
     None if n_hys == 0 else red_bold),
    ("Mikrobiologie pozitivni", f"{n_mic}/{len(M)}",
     ok_fill if n_mic == 0 else problem_fill, link(N_MICRO, 3),
     None if n_mic == 0 else red_bold),
    ("Datove mezery (DILI/panely)", len(gap_rows), attn_fill, link(N_GAP, 2), None),
    ("Zrusene testy", len(cancel_std) + len(cancel_mic), attn_fill, link(N_CANCEL, 4), None),
    ("Revize dat (R)", len(rev_rows), attn_fill, link(N_ZDROJ, 2, "P"), None),
]
row = 4
for label, value, fill, tgt, vfont in tiles:
    lc = ws.cell(row=row, column=1, value=label)
    lc.font = bold_font
    lc.fill = fill
    lc.border = border
    lc.alignment = LEFT
    vc = ws.cell(row=row, column=2, value=value)
    vc.fill = fill
    vc.border = border
    vc.alignment = CENTER
    vc.hyperlink = tgt
    vc.font = vfont or link_font
    ws.row_dimensions[row].height = 20
    row += 1

# Mini-pivot: abnormality dle studie a smeru.
row += 1
ws.cell(row=row, column=1, value="Abnormality dle studie a smeru").font = bold_font
row += 1
for ci, h in enumerate(["Studie", "Vysoke (H*)", "Nizke (L*)", "Kriticke"], 1):
    hc = ws.cell(row=row, column=ci, value=h)
    hc.font = header_font
    hc.fill = header_fill
    hc.border = border
    hc.alignment = CENTER
row += 1
for st in sorted({d["study"] for d in abn_rows}):
    rs = [d for d in abn_rows if d["study"] == st]
    vals = [STUDY_LABEL.get(st, st),
            sum(1 for d in rs if d["direction"] == "H"),
            sum(1 for d in rs if d["direction"] == "L"),
            sum(1 for d in rs if d["sev"] == 3)]
    for ci, v in enumerate(vals, 1):
        x = ws.cell(row=row, column=ci, value=v)
        x.border = border
        x.alignment = CENTER
        x.font = data_font
        if ci >= 2:
            x.hyperlink = link(N_ABN, 2)
            x.font = link_font
    row += 1


# =============================================================================
# LIST 2: SOUHRN CENTER
# =============================================================================
ws = ws_souhrn
sc_cols = [
    ("Studie", 12), ("Centrum", 12), ("Pacient", 16), ("Pohlavi", 10),
    ("Vysledku", 11), ("Abnormalni", 12), ("Kriticke", 11), ("Mezery", 10),
    ("Zrusene", 10), ("Revize", 9), ("Prvni sber", 14), ("Posledni sber", 14),
]
write_header(ws, 1, [h for h, _ in sc_cols], [w for _, w in sc_cols])
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(sc_cols))}1"

def sc_count_cell(ws, r, ci, val, warn=False, crit=False, good=False):
    x = ws.cell(row=r, column=ci, value=val)
    x.border = border
    x.alignment = CENTER
    if crit and val > 0:
        x.fill = problem_fill
        x.font = red_bold
    elif warn and val > 0:
        x.fill = attn_fill
        x.font = data_font
    elif good and val == 0:
        x.fill = ok_fill
        x.font = data_font
    elif val == 0:
        x.font = muted_font
    else:
        x.font = data_font
    return x

r = 2
prev_site = None
sub_tot = defaultdict(int)
grand = defaultdict(int)
def write_subtotal(ws, r, site_label):
    vals = ["", f"MEZISOUCET {site_label}", "", "",
            sub_tot["n"], sub_tot["abn"], sub_tot["crit"], sub_tot["gap"],
            sub_tot["cancel"], sub_tot["rev"], "", ""]
    for ci, v in enumerate(vals, 1):
        x = ws.cell(row=r, column=ci, value=v)
        x.fill = total_fill
        x.border = border
        x.font = bold_font
        x.alignment = LEFT if ci == 2 else CENTER
    return r + 1

for k in souhrn_keys:
    if prev_site is not None and (k[0], k[1]) != prev_site:
        r = write_subtotal(ws, r, f"{STUDY_LABEL.get(prev_site[0], prev_site[0])}/{prev_site[1]}")
        sub_tot = defaultdict(int)
    a = agg[k]
    cell(ws, r, 1, STUDY_LABEL.get(k[0], k[0]))
    cell(ws, r, 2, k[1])
    pac = cell(ws, r, 3, k[2], align=LEFT)
    tgt = abn_first_subject.get(k)
    if tgt:
        pac.hyperlink = link(N_ABN, tgt)
        pac.font = link_font
    g = next((d["gender"] for d in S if d["subject"] == k[2]), None)
    cell(ws, r, 4, g)
    cell(ws, r, 5, a["n"])
    ab = sc_count_cell(ws, r, 6, a["abn"], warn=True, good=True)
    if tgt and a["abn"]:
        ab.hyperlink = link(N_ABN, tgt)
        ab.font = link_font if a["crit"] == 0 else red_bold
    sc_count_cell(ws, r, 7, a["crit"], crit=True)
    sc_count_cell(ws, r, 8, a["gap"], warn=True)
    sc_count_cell(ws, r, 9, a["cancel"], warn=True)
    sc_count_cell(ws, r, 10, a["rev"], warn=True)
    cell(ws, r, 11, a["dmin"], numfmt=DATE_FMT)
    cell(ws, r, 12, a["dmax"], numfmt=DATE_FMT)
    for key in ("n", "abn", "crit", "gap", "cancel", "rev"):
        sub_tot[key] += a[key]
        grand[key] += a[key]
    prev_site = (k[0], k[1])
    r += 1
if prev_site is not None:
    r = write_subtotal(ws, r, f"{STUDY_LABEL.get(prev_site[0], prev_site[0])}/{prev_site[1]}")
# Grand total.
gvals = ["", "CELKEM", "", "", grand["n"], grand["abn"], grand["crit"],
         grand["gap"], grand["cancel"], grand["rev"], "", ""]
for ci, v in enumerate(gvals, 1):
    x = ws.cell(row=r, column=ci, value=v)
    x.fill = total_fill
    x.border = border
    x.font = Font(name="Calibri", bold=True, size=12)
    x.alignment = LEFT if ci == 2 else CENTER


# =============================================================================
# LIST 3: DILI Hyuv zakon
# =============================================================================
ws = ws_dili
ws.merge_cells("A1:H1")
t = ws.cell(row=1, column=1, value="DILI / HYUV ZAKON")
t.fill = title_fill
t.font = title_font
t.alignment = LEFT
ws.merge_cells("A2:H2")
v = ws.cell(row=2, column=1)
if hys_subjects:
    v.value = f"POZOR: {len(hys_subjects)} pacient(u) splnuje Hyuv zakon"
    v.fill = problem_fill
    v.font = red_bold
else:
    v.value = "Zadny pacient nesplnuje kriteria Hyova zakona"
    v.fill = ok_fill
    v.font = bold_font
v.alignment = LEFT
ws.row_dimensions[1].height = 24

dili_cols = [("Studie", 12), ("Centrum", 12), ("Pacient", 16), ("Accession", 14),
             ("Navsteva", 22), ("Datum", 14), ("Kriterium", 30), ("Hodnota", 22)]
write_header(ws, 4, [h for h, _ in dili_cols], [w for _, w in dili_cols])
ws.freeze_panes = "A5"
ws.auto_filter.ref = f"A4:{get_column_letter(len(dili_cols))}4"

dili_sorted = sorted([d for d in S if d["is_dili"]],
                     key=lambda d: (0 if is_criteria_met(d["value"]) and d["tg"] in DILI_DANGER else 1,
                                    d["study"] or "", d["site"] or "", d["subject"] or "",
                                    d["collected"] or MINDT))
r = 5
for d in dili_sorted:
    cell(ws, r, 1, d["studyl"])
    cell(ws, r, 2, d["site"])
    pac = cell(ws, r, 3, d["subject"], align=LEFT)
    sr = souhrn_row_by_ssp.get((d["study"], d["site"], d["subject"]))
    if sr:
        pac.hyperlink = link(N_SOUHRN, sr)
        pac.font = link_font
    acc = cell(ws, r, 4, d["accession"])
    acc.hyperlink = link(N_ZDROJ, zdroj_row[d["id"]])
    acc.font = link_font
    cell(ws, r, 5, d["visit"], align=LEFT)
    cell(ws, r, 6, d["collected"], numfmt=DATE_FMT)
    cell(ws, r, 7, d["tg"], align=LEFT)
    hc = cell(ws, r, 8, d["value"], align=LEFT)
    met = is_criteria_met(d["value"])
    if met and d["tg"] in DILI_DANGER:
        for ci in range(1, 9):
            ws.cell(row=r, column=ci).fill = problem_fill
        hc.font = red_bold
    elif met and d["tg"] == "ALP<2XULN":
        hc.fill = attn_fill  # cholestaza-exclusion, samo o sobe NENI pozitivni
    elif str(d["value"]).strip().lower() in GAP_VALUES:
        hc.fill = attn_fill
    elif str(d["value"]).strip().lower() == "criteria not met":
        hc.fill = ok_fill
    r += 1


# =============================================================================
# LIST 4: Abnormality
# =============================================================================
ws = ws_abn
abn_cols = [
    ("Zavaznost", 11), ("Studie", 11), ("Centrum", 11), ("Pacient", 16),
    ("Pohlavi", 9), ("Navsteva", 20), ("Datum odberu", 14), ("Accession", 14),
    ("Panel", 26), ("Test", 24), ("Hodnota", 12), ("Jednotky", 11),
    ("Rozmezi", 13), ("Smer", 9), ("Mimo o %", 11), ("Priznak", 9),
    ("Kvalifikator", 16), ("Komentar", 22),
]
write_header(ws, 1, [h for h, _ in abn_cols], [w for _, w in abn_cols])
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(abn_cols))}1"

QUAL_EXP = {"T": "kriticka hodnota", "SF": "SF", "RX": "RX"}
r = 2
for d in abn_sorted:
    sev = d["sev"]
    row_fill = problem_fill if sev == 3 else (attn_fill if sev == 2 else None)
    cell(ws, r, 1, SEV_LABEL[sev], font=bold_font if sev == 3 else data_font)
    cell(ws, r, 2, d["studyl"])
    cell(ws, r, 3, d["site"])
    pac = cell(ws, r, 4, d["subject"], align=LEFT)
    sr = souhrn_row_by_ssp.get((d["study"], d["site"], d["subject"]))
    if sr:
        pac.hyperlink = link(N_SOUHRN, sr)
        pac.font = link_font
    cell(ws, r, 5, d["gender"])
    cell(ws, r, 6, d["visit"], align=LEFT)
    cell(ws, r, 7, d["collected"], numfmt=DATE_FMT)
    acc = cell(ws, r, 8, d["accession"])
    acc.hyperlink = link(N_ZDROJ, zdroj_row[d["id"]])
    acc.font = link_font
    cell(ws, r, 9, d["tg"], align=LEFT)
    cell(ws, r, 10, d["test"], align=LEFT)
    hc = cell(ws, r, 11, d["vnum"] if d["vnum"] is not None else d["value"])
    hc.font = red_bold if sev == 3 else red_font
    cell(ws, r, 12, d["units"])
    cell(ws, r, 13, d["range"])
    sm = cell(ws, r, 14, "Vysoke" if d["direction"] == "H" else "Nizke")
    sm.font = red_font
    cell(ws, r, 15, round(d["opct"], 1) if d["opct"] is not None else None)
    cell(ws, r, 16, d["flags"])
    qexp = ", ".join(QUAL_EXP.get(q, q) for q in sorted(d["quals"]))
    cell(ws, r, 17, qexp or None, font=bold_font if "T" in d["quals"] else data_font, align=LEFT)
    cell(ws, r, 18, d["comments"], align=LEFT)
    if row_fill:
        for ci in range(1, len(abn_cols) + 1):
            if ws.cell(row=r, column=ci).fill.fgColor.rgb in ("00000000", None):
                ws.cell(row=r, column=ci).fill = row_fill
    r += 1


# =============================================================================
# LIST 5: Matice pokryti
# =============================================================================
ws = ws_matice
present_visits = [v for v in VISIT_ORDER if any(d["visit"] == v for d in S)]
extra = sorted({d["visit"] for d in S if d["visit"] and d["visit"] not in VISIT_ORDER})
present_visits += extra
# Skupinove pasy (row 1).
ws.cell(row=1, column=1, value="").fill = header_fill
groups_seq = []
for v in present_visits:
    g = visit_group(v)
    if not groups_seq or groups_seq[-1][0] != g:
        groups_seq.append([g, 1])
    else:
        groups_seq[-1][1] += 1
col = 3
for g, span in groups_seq:
    ws.merge_cells(start_row=1, start_column=col, end_row=1, end_column=col + span - 1)
    gc = ws.cell(row=1, column=col, value=g)
    gc.font = bold_font
    gc.alignment = CENTER
    gc.fill = total_fill
    for cc in range(col, col + span):
        ws.cell(row=1, column=cc).border = border
    col += span
# Hlavicka (row 2).
hdr = ["Centrum", "Pacient"] + present_visits
write_header(ws, 2, hdr,
             [12, 16] + [max(10, min(16, len(v))) for v in present_visits])
ws.freeze_panes = "C3"

mat = defaultdict(lambda: {"n": 0, "abn": 0, "crit": 0, "cancel": 0, "rev": 0})
for d in S:
    m = mat[(d["subject"], d["visit"])]
    m["n"] += 1
    if d["direction"]:
        m["abn"] += 1
    if d["sev"] == 3:
        m["crit"] += 1
    if d["cancel"]:
        m["cancel"] += 1
    if d["rev"]:
        m["rev"] += 1

mat_subjects = sorted({(d["study"], d["site"], d["subject"]) for d in S},
                      key=lambda k: (k[0] or "", k[1] or "", k[2] or ""))
r = 3
for st, site, subj in mat_subjects:
    cell(ws, r, 1, site)
    cell(ws, r, 2, subj, align=LEFT)
    for ci, v in enumerate(present_visits, 3):
        m = mat.get((subj, v))
        if not m:
            # Prazdna bunka = navsteva bez dat. Necervenime ji — bez planu navstev
            # per pacient nevime, zda byla ocekavana (screening-only pacient nema
            # Week 12). Jen ramecek, vizualne neutralni.
            x = ws.cell(row=r, column=ci, value=None)
            x.border = border
            continue
        disp = str(m["n"])
        if m["cancel"]:
            disp += f" (x{m['cancel']})"
        if m["rev"]:
            disp += " R"
        x = ws.cell(row=r, column=ci, value=disp)
        x.border = border
        x.alignment = CENTER
        if m["crit"]:
            x.fill = problem_fill
            x.font = red_bold
        elif m["abn"]:
            x.fill = attn_fill
            x.font = red_font
        elif m["cancel"]:
            x.fill = attn_fill
            x.font = data_font
        else:
            x.fill = ok_fill
            x.font = data_font
        fr = zdroj_first_sv.get((subj, v))
        if fr:
            x.hyperlink = link(N_ZDROJ, fr)
    r += 1


# =============================================================================
# LIST 6: Zrusene testy
# =============================================================================
ws = ws_cancel
# Pivot duvodu (rows 1..N).
reason_counts = defaultdict(int)
for d in cancel_std + cancel_mic:
    reason_counts[d["cancel"]] += 1
ws.cell(row=1, column=1, value="Duvody zruseni").font = bold_font
pr = 2
for reason, n in sorted(reason_counts.items(), key=lambda x: -x[1]):
    rc = ws.cell(row=pr, column=1, value=reason)
    rc.fill = attn_fill
    rc.border = border
    rc.alignment = LEFT
    rc.font = data_font
    nc = ws.cell(row=pr, column=2, value=n)
    nc.fill = attn_fill
    nc.border = border
    nc.alignment = CENTER
    nc.font = data_font
    pr += 1
hdr_row = pr + 1
cancel_cols = [("Studie", 12), ("Centrum", 12), ("Pacient", 16), ("Navsteva", 22),
               ("Datum", 14), ("Panel", 26), ("Test", 24), ("Duvod zruseni", 26),
               ("Accession", 14), ("Komentar", 22)]
write_header(ws, hdr_row, [h for h, _ in cancel_cols], [w for _, w in cancel_cols])
ws.freeze_panes = f"A{hdr_row + 1}"
ws.auto_filter.ref = f"A{hdr_row}:{get_column_letter(len(cancel_cols))}{hdr_row}"
LOST_RE = re.compile(r"(?i)(no specimen|quantity not sufficient|beyond stability|not received|qns)")
cancel_all = sorted(cancel_std + cancel_mic,
                    key=lambda d: (d["study"] or "", d["site"] or "", d["subject"] or "",
                                   d["cancel"] or ""))
r = hdr_row + 1
for d in cancel_all:
    for ci in range(1, len(cancel_cols) + 1):
        ws.cell(row=r, column=ci).fill = attn_fill
    cell(ws, r, 1, d["studyl"], fill=attn_fill)
    cell(ws, r, 2, d["site"], fill=attn_fill)
    pac = cell(ws, r, 3, d["subject"], fill=attn_fill, align=LEFT)
    sr = souhrn_row_by_ssp.get((d["study"], d["site"], d["subject"]))
    if sr:
        pac.hyperlink = link(N_SOUHRN, sr)
        pac.font = link_font
    cell(ws, r, 4, d["visit"], fill=attn_fill, align=LEFT)
    cell(ws, r, 5, d["collected"], fill=attn_fill, numfmt=DATE_FMT)
    cell(ws, r, 6, d.get("tg"), fill=attn_fill, align=LEFT)
    cell(ws, r, 7, d.get("test"), fill=attn_fill, align=LEFT)
    rc = cell(ws, r, 8, d["cancel"], fill=attn_fill, align=LEFT)
    if d["cancel"] and LOST_RE.search(str(d["cancel"])):
        rc.font = red_font
    ac = cell(ws, r, 9, d["accession"], fill=attn_fill)
    if d["id"] in zdroj_row:
        ac.hyperlink = link(N_ZDROJ, zdroj_row[d["id"]])
        ac.font = link_font
    cell(ws, r, 10, d["comments"], fill=attn_fill, align=LEFT)
    r += 1


# =============================================================================
# LIST 7: Mikrobiologie
# =============================================================================
ws = ws_micro
ws.merge_cells("A1:N1")
mb = ws.cell(row=1, column=1)
if micro_pos:
    mb.value = f"{len(micro_pos)} pozitivnich kultivaci"
    mb.fill = problem_fill
    mb.font = red_bold
else:
    mb.value = "Zadne pozitivni kultivace"
    mb.fill = ok_fill
    mb.font = bold_font
mb.alignment = LEFT
micro_cols = [("Centrum", 12), ("Pacient", 16), ("Navsteva", 24), ("Datum", 14),
              ("Accession", 14), ("Panel", 18), ("Vzorek", 12), ("Organismus", 16),
              ("Rust", 12), ("Vysledek", 28), ("Stav reportu", 16),
              ("Zruseno", 18), ("MIC", 14), ("Disk", 14)]
write_header(ws, 2, [h for h, _ in micro_cols], [w for _, w in micro_cols])
ws.freeze_panes = "A3"
ws.auto_filter.ref = f"A2:{get_column_letter(len(micro_cols))}2"
micro_sorted = sorted(M, key=lambda d: (0 if d["positive"] else 1,
                                        d["site"] or "", d["subject"] or "",
                                        d["collected"] or MINDT))
r = 3
for d in micro_sorted:
    f = d["f"]
    pos = d["positive"]
    pending = not f.get("Test Result") and not f.get("Test Description")
    if pos:
        row_fill, fnt = problem_fill, red_bold
    elif d["cancel"] or pending:
        row_fill, fnt = attn_fill, data_font
    else:
        row_fill, fnt = ok_fill, data_font
    vals = [d["site"], d["subject"], d["visit"], None, d["accession"],
            f.get("Test Group"), f.get("Specimen"), f.get("Organism"),
            f.get("Growth"), f.get("Test Result"), f.get("Test Description"),
            d["cancel"],
            " ".join(x for x in [f.get("MIC Result"), f.get("MIC Interpretation")] if x) or None,
            f.get("Disk Diffusion Interpretation")]
    for ci, v in enumerate(vals, 1):
        x = cell(ws, r, ci, v, font=fnt, fill=row_fill,
                 align=LEFT if ci in (3, 8, 9, 10, 11) else CENTER)
        if ci == 4:
            x.value = d["collected"]
            x.number_format = DATE_FMT
    r += 1


# =============================================================================
# LIST 8: Datove mezery
# =============================================================================
ws = ws_gaps
gap_cols = [("Studie", 12), ("Centrum", 12), ("Pacient", 16), ("Navsteva", 22),
            ("Datum", 14), ("Panel", 26), ("Test", 24), ("Problem", 28)]
write_header(ws, 1, [h for h, _ in gap_cols], [w for _, w in gap_cols])
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(gap_cols))}1"
gap_sorted = sorted(gap_rows, key=lambda d: (d["study"] or "", d["site"] or "",
                                             d["subject"] or "", d["collected"] or MINDT))
r = 2
for d in gap_sorted:
    for ci in range(1, len(gap_cols) + 1):
        ws.cell(row=r, column=ci).fill = attn_fill
    cell(ws, r, 1, d["studyl"], fill=attn_fill)
    cell(ws, r, 2, d["site"], fill=attn_fill)
    cell(ws, r, 3, d["subject"], fill=attn_fill, align=LEFT)
    cell(ws, r, 4, d["visit"], fill=attn_fill, align=LEFT)
    cell(ws, r, 5, d["collected"], fill=attn_fill, numfmt=DATE_FMT)
    cell(ws, r, 6, d["tg"], fill=attn_fill, align=LEFT)
    cell(ws, r, 7, d["test"], fill=attn_fill, align=LEFT)
    if d["value"] and str(d["value"]).strip().lower() in GAP_VALUES:
        problem = d["value"]
    else:
        problem = "Prazdna hodnota v klicovem panelu"
    cell(ws, r, 8, problem, fill=attn_fill, align=LEFT)
    r += 1


# =============================================================================
# LIST 10: LEGENDA
# =============================================================================
ws = ws_legend
ws.column_dimensions["A"].width = 26
ws.column_dimensions["B"].width = 60
lr = 1
def legend_title(text):
    global lr
    ws.merge_cells(start_row=lr, start_column=1, end_row=lr, end_column=2)
    c = ws.cell(row=lr, column=1, value=text)
    c.fill = title_fill
    c.font = Font(name="Calibri", bold=True, size=12, color="FFFFFF")
    c.alignment = LEFT
    lr += 1
def legend_row(a, b, fill=None):
    global lr
    ca = ws.cell(row=lr, column=1, value=a)
    ca.border = border
    ca.alignment = LEFT
    ca.font = bold_font
    if fill:
        ca.fill = fill
    cb = ws.cell(row=lr, column=2, value=b)
    cb.border = border
    cb.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
    cb.font = data_font
    lr += 1

legend_title("Priznaky (Flags)")
for a, b in [("H / L", "Hodnota nad / pod referencnim rozmezim"),
             ("HT / LT", "Kriticka (panic) hodnota — vysoka / nizka"),
             ("SF", "Vysledek mimo ocekavani (see footnote)"),
             ("RX", "Doplnujici/opakovane vysetreni")]:
    legend_row(a, b)
lr += 1
legend_title("Zavaznost (list Abnormality)")
legend_row("Kriticka", "Panic flag (T), >100 % mimo rozmezi, nebo jaterni H / cytopenie L", problem_fill)
legend_row("Vysoka", ">=50 % mimo rozmezi", attn_fill)
legend_row("Mirna", "Mimo rozmezi do 50 %")
lr += 1
legend_title("Barvy")
legend_row("Cervena", "Bezpecnostni signal / kriticke / chybejici data", problem_fill)
legend_row("Zluta", "Pozornost — abnormalni, zrusene, mezery, revize", attn_fill)
legend_row("Zelena", "V poradku / bez nalezu", ok_fill)
legend_row("Modra", "Soucty a celkove radky", total_fill)
lr += 1
legend_title("Hyuv zakon (DILI)")
legend_row("Pozitivni", "ALT nebo AST >=3x ULN 'Criteria Met' SOUCASNE s TBIL >=2x ULN kombo")
legend_row("ALP<2XULN", "Sam o sobe NENI pozitivni (cholestaza-exclusion), proto jen zluta")
lr += 1
legend_title("Studie")
legend_row("36940", "Protokol 77242113UCO3001 (UCO3001)")
legend_row("35472", "Protokol 42847922MDD3003 (MDD3003)")
lr += 1
legend_title("Aktualnost dat")
if dt_min and dt_max:
    legend_row("Sber", f"{dt_min.strftime('%d-%b-%Y')} – {dt_max.strftime('%d-%b-%Y')}")
legend_row("Zdroj", "MongoDB covance.results (import z Labcorp XSP CSV)")


# -- Ulozeni ------------------------------------------------------------------
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
out_path = OUT_DIR + f"{timestamp} Covance lab results report.xlsx"
wb.save(out_path)
client.close()

print(f"\nUlozeno: {out_path}")
print(f"  Vysledku celkem: {len(all_docs)} (standard {len(S)}, micro {len(M)})")
print(f"  Abnormalni: {len(abn_rows)} (kriticke {len(crit_rows)}), "
      f"zrusene {len(cancel_std) + len(cancel_mic)}, mezery {len(gap_rows)}, "
      f"revize {len(rev_rows)}")
print(f"  Hyuv zakon pozitivni: {len(hys_subjects)}, mikrobiologie pozitivni: {len(micro_pos)}")
print(f"  Centra: {len(sites)}, pacienti: {len(subjects)}")
