"""
MASLDTrialReadQC-Kor — MASH RCT 판독 데이터 품질 감시 (RBQM) 도구
Streamlit 메인 진입점.

도메인: MASLD (대사성간질환) / MASH RCT
카테고리: 인체실험 도구 — RBQM / central statistical monitoring

본 파일은 UI 만 담당하며 모든 분석 로직은 analysis.py 에 있다.
오프라인 standalone — 외부 네트워크/API/CDN 호출 없음.
"""

import os

import numpy as np
import pandas as pd
import streamlit as st
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

import analysis

HERE = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(HERE, "data")

st.set_page_config(page_title="MASLDTrialReadQC-Kor", layout="wide")

# ----------------------------------------------------------------------
# 디스클레이머 (의학적 안전성)
# ----------------------------------------------------------------------
st.title("MASLDTrialReadQC-Kor — MASH RCT 판독 품질 감시 도구")
st.caption("MASLD 도메인 · RBQM / Central Statistical Monitoring · 연구용 MVP")

st.warning(
    "**면책 / 안전성 고지**  \n"
    "본 도구는 **연구용·참고용**이며, 실제 임상시험 모니터링 의사결정은 "
    "자격을 갖춘 담당자(임상시험 모니터, 통계가, central reader 등)가 수행해야 합니다.  \n"
    "NIT 품질 기준·자연 변화율 등 수치 임계값은 **예시적·교육용**이며, ICH E6(R2)/"
    "E6(R3), FDA RBM guidance, AASLD/EASL MASLD 가이드라인 등 **최신 문서로 반드시 "
    "검증**해야 합니다. 기본 제공 데이터는 전적으로 **합성 데이터**입니다."
)


# ----------------------------------------------------------------------
# 데이터 로딩 (업로드 또는 번들 합성 데이터)
# ----------------------------------------------------------------------
@st.cache_data
def load_bundled():
    return analysis.load_data(DATA_DIR)


st.sidebar.header("데이터")
src = st.sidebar.radio(
    "데이터 소스",
    ["번들 합성 데이터", "CSV 업로드"],
    help="업로드 시 measurements.csv 와 biopsy.csv 가 필요합니다.",
)

data = None
if src == "번들 합성 데이터":
    data = load_bundled()
    st.sidebar.success(
        f"합성 데이터 로드 완료\n\n"
        f"measurements: {len(data['measurements'])}행\n"
        f"biopsy: {len(data['biopsy'])}행"
    )
else:
    up_meas = st.sidebar.file_uploader("measurements.csv", type="csv")
    up_biop = st.sidebar.file_uploader("biopsy.csv", type="csv")
    if up_meas is not None and up_biop is not None:
        meas = pd.read_csv(up_meas)
        biopsy = pd.read_csv(up_biop)
        data = {"measurements": meas, "biopsy": biopsy,
                "sites": pd.DataFrame()}
        st.sidebar.success("업로드 데이터 로드 완료")
    else:
        st.info("좌측에서 measurements.csv 와 biopsy.csv 를 업로드하세요.")
        st.stop()

st.sidebar.markdown("---")
st.sidebar.caption(
    "NIT = non-invasive test. VCTE LSM/CAP · MRI-PDFF · MRE · FIB-4 · ELF · biopsy."
)

# 전체 분석 1회 실행 (캐시)
report = analysis.build_rbqm_report(data)
meas = data["measurements"]
biopsy = data["biopsy"]


# ----------------------------------------------------------------------
# 탭 구성 (핵심 기능 5개)
# ----------------------------------------------------------------------
tab1, tab2, tab3, tab4, tab5 = st.tabs([
    "1. 측정 품질",
    "2. Reader drift",
    "3. 사이트/스캐너 bias",
    "4. Implausible · biopsy 일치도",
    "5. RBQM 리포트",
])


# ===== 탭 1: 측정 품질 ===================================================
with tab1:
    st.header("1. NIT·영상·biopsy ingest + 측정 품질 플래그")
    st.markdown(
        "VCTE IQR/median ratio·유효측정 수 기준 위반을 자동 플래그합니다. "
        f"기준(예시·교육용): IQR/median < "
        f"**{analysis.QC_THRESHOLDS['vcte_iqr_median_max']:.0%}**, "
        f"유효측정 ≥ **{analysis.QC_THRESHOLDS['vcte_valid_min']}**회."
    )

    flagged = report["flagged"]
    qsum = report["quality_summary"]

    c1, c2, c3 = st.columns(3)
    c1.metric("전체 VCTE 측정", f"{len(flagged)}건")
    c2.metric("품질기준 위반", f"{int(flagged['qc_any_fail'].sum())}건")
    c3.metric("위반율", f"{flagged['qc_any_fail'].mean():.1%}")

    st.subheader("사이트별 품질 요약")
    st.dataframe(qsum, use_container_width=True)

    st.subheader("사이트별 VCTE 품질 실패율")
    fig, ax = plt.subplots(figsize=(8, 3.5))
    colors = ["#c0392b" if v >= 0.1 else "#3498db" for v in qsum["qc_fail_rate"]]
    ax.bar(qsum["site_id"], qsum["qc_fail_rate"], color=colors)
    ax.set_ylabel("QC 실패율")
    ax.set_xlabel("site_id")
    ax.axhline(0.1, color="orange", ls="--", lw=1, label="검토 임계 10%")
    ax.legend()
    st.pyplot(fig)
    plt.close(fig)

    st.subheader("품질 위반 측정 상세")
    fails = flagged[flagged["qc_any_fail"]][
        ["patient_id", "site_id", "reader_id", "visit",
         "vcte_iqr_median_ratio", "vcte_valid_measurements", "qc_reason"]
    ]
    st.dataframe(fails, use_container_width=True, height=300)
    st.caption(f"위반 {len(fails)}건. NIT 원자료는 측정별 품질 메타데이터와 함께 ingest 됩니다.")


# ===== 탭 2: Reader drift ===============================================
with tab2:
    st.header("2. Central reader 종단 drift 탐지")
    st.markdown(
        "각 central reader 의 판독값(잔차) 시간 추세를 회귀로 추정하고, "
        "**reader 간 상대 편차(robust z)** 로 systematic drift 를 플래그합니다. "
        "환자 평균 centering 으로 case-mix 를 보정합니다."
    )

    metric = st.selectbox(
        "drift 분석 지표",
        ["vcte_lsm_kpa", "mri_pdff_pct", "mre_kpa", "fib4", "elf", "vcte_cap_dbm"],
        index=0,
    )
    drift = analysis.reader_drift(meas, metric=metric)

    st.subheader("Reader 별 drift 통계")
    st.dataframe(drift, use_container_width=True)

    drifted = drift[drift["drift_flag"]]
    if len(drifted):
        st.error(
            "DRIFT 플래그 reader: "
            + ", ".join(
                f"{r.reader_id} (기울기 {r.slope_per_week:+.4f}/주, "
                f"robust z {r.robust_z:+.1f}, {r.direction})"
                for r in drifted.itertuples()
            )
        )
    else:
        st.success("현재 임계값 기준 유의한 reader drift 미탐지.")

    st.subheader("Reader 별 기울기 비교")
    fig, ax = plt.subplots(figsize=(8, 3.5))
    valid = drift.dropna(subset=["slope_per_week"])
    colors = ["#c0392b" if f else "#3498db" for f in valid["drift_flag"]]
    ax.bar(valid["reader_id"], valid["slope_per_week"], color=colors)
    ax.axhline(0, color="gray", lw=1)
    ax.set_ylabel(f"{metric} 기울기 (/주)")
    ax.set_xlabel("reader_id")
    st.pyplot(fig)
    plt.close(fig)

    sel_reader = st.selectbox("잔차 산점도 reader", list(drift["reader_id"]))
    pts = analysis.reader_drift_points(meas, sel_reader, metric=metric)
    if len(pts) >= 2:
        fig, ax = plt.subplots(figsize=(8, 3.5))
        ax.scatter(pts["week"], pts["residual"], alpha=0.5, color="#2c3e50")
        lr = np.polyfit(pts["week"], pts["residual"], 1)
        xs = np.array([pts["week"].min(), pts["week"].max()])
        ax.plot(xs, np.polyval(lr, xs), color="#c0392b", lw=2,
                label=f"기울기 {lr[0]:+.4f}/주")
        ax.axhline(0, color="gray", lw=1, ls=":")
        ax.set_xlabel("trial week")
        ax.set_ylabel(f"{metric} 잔차 (환자 평균 대비)")
        ax.set_title(f"reader {sel_reader} 종단 잔차")
        ax.legend()
        st.pyplot(fig)
        plt.close(fig)


# ===== 탭 3: 사이트/스캐너 bias ==========================================
with tab3:
    st.header("3. 사이트/스캐너 systematic bias (funnel plot)")
    st.markdown(
        "case-mix(age·bmi·week) 보정 후 사이트·스캐너별 평균 잔차를 비교합니다. "
        "funnel 의 ±2 SD 한계를 벗어나면 calibration outlier 로 플래그합니다."
    )

    grp = st.radio("그룹 단위", ["site_id", "scanner_id"], horizontal=True)
    bmetric = st.selectbox(
        "bias 분석 지표",
        ["vcte_lsm_kpa", "mri_pdff_pct", "mre_kpa", "fib4", "elf"],
        index=0, key="bias_metric",
    )
    bias = analysis.site_scanner_bias(meas, metric=bmetric, group=grp)

    st.subheader(f"{grp} 별 bias 통계")
    st.dataframe(bias, use_container_width=True)

    outliers = bias[bias["outlier_flag"]]
    if len(outliers):
        st.error(
            "Calibration outlier: "
            + ", ".join(
                f"{getattr(r, grp)} (평균잔차 {r.mean_residual:+.2f}, z {r.z_score:+.1f})"
                for r in outliers.itertuples()
            )
        )
    else:
        st.success("calibration outlier 미탐지.")

    st.subheader("Funnel plot")
    fig, ax = plt.subplots(figsize=(8, 4.5))
    ax.scatter(bias["n"], bias["mean_residual"],
               color=["#c0392b" if f else "#3498db" for f in bias["outlier_flag"]],
               s=60, zorder=3)
    for r in bias.itertuples():
        ax.annotate(getattr(r, grp), (r.n, r.mean_residual),
                    textcoords="offset points", xytext=(6, 4), fontsize=8)
    # funnel 한계: 평균잔차 0 기준 ±z*SE
    ns = np.linspace(max(1, bias["n"].min() * 0.8), bias["n"].max() * 1.1, 50)
    resid_sd = (meas[bmetric] - meas[bmetric].mean()).std(ddof=1)
    z = analysis.QC_THRESHOLDS["site_bias_z"]
    ax.plot(ns, z * resid_sd / np.sqrt(ns), color="orange", ls="--", lw=1)
    ax.plot(ns, -z * resid_sd / np.sqrt(ns), color="orange", ls="--", lw=1,
            label=f"±{z} SD funnel")
    ax.axhline(0, color="gray", lw=1)
    ax.set_xlabel(f"{grp} 측정 수 (n)")
    ax.set_ylabel(f"{bmetric} 평균 잔차 (case-mix 보정)")
    ax.legend()
    st.pyplot(fig)
    plt.close(fig)


# ===== 탭 4: implausible + biopsy 일치도 =================================
with tab4:
    st.header("4. Implausible 변화율 + biopsy 일치도 drift")

    st.subheader("4a. 생물학적으로 implausible 한 종단 변화")
    st.markdown(
        "인접 방문 간 LSM·PDFF 변화율이 생물학적 한계를 초과해 급감하거나, "
        "비단조(V자/역V자) 패턴을 보이는 환자를 플래그합니다."
    )
    impl = report["implausible"]
    st.metric("implausible 변화 플래그", f"{len(impl)}건")
    if len(impl):
        st.dataframe(impl, use_container_width=True, height=260)
    else:
        st.success("implausible 종단 변화 미탐지.")

    st.markdown("---")
    st.subheader("4b. Central pathologist 간 biopsy 일치도 drift")
    st.markdown(
        "두 central pathologist 의 fibrosis stage (가중 κ) 와 NAS (ICC) 일치도를 "
        "trial 등록 구간(EARLY/LATE)별로 추적합니다."
    )
    ag = report["biopsy_agreement"]
    c1, c2 = st.columns(2)
    c1.metric("전체 fibrosis 가중 κ", f"{ag['overall_kappa']}")
    c2.metric("전체 NAS ICC", f"{ag['overall_icc']}")

    st.dataframe(ag["by_block"], use_container_width=True)

    fig, ax = plt.subplots(figsize=(7, 3.5))
    bb = ag["by_block"]
    x = np.arange(len(bb))
    w = 0.35
    ax.bar(x - w / 2, bb["fibrosis_weighted_kappa"], w, label="fibrosis 가중 κ",
           color="#3498db")
    ax.bar(x + w / 2, bb["nas_icc"], w, label="NAS ICC", color="#16a085")
    ax.set_xticks(x)
    ax.set_xticklabels(bb["enroll_block"])
    ax.axhline(0.6, color="orange", ls="--", lw=1, label="양호 기준 0.6")
    ax.set_ylabel("일치도")
    ax.legend()
    st.pyplot(fig)
    plt.close(fig)

    if ag["drift_flag"]:
        st.error(f"일치도 DRIFT 경고: {ag['drift_detail']}")
    else:
        st.success("구간 간 일치도 저하 미탐지.")


# ===== 탭 5: RBQM 리포트 =================================================
with tab5:
    st.header("5. RBQM 리포트 + reader 재교정 권고")
    st.markdown(
        "ICH E6 central monitoring 형식으로 사이트/reader risk 순위, 품질 실패율, "
        "drift 패턴, 권고 조치를 종합한 리포트를 생성합니다."
    )

    st.subheader("사이트 risk 순위")
    sr = report["site_risk"]
    st.dataframe(
        sr[["site_id", "n_measurements", "qc_fail_rate", "z_score",
            "outlier_flag", "risk_score", "risk_level"]],
        use_container_width=True,
    )

    st.subheader("권고 조치")
    for rec in analysis.build_recommendations(report):
        st.markdown(f"- {rec}")

    st.subheader("텍스트 리포트 (ICH E6 형식)")
    report_text = analysis.render_report_text(report)
    st.code(report_text, language="text")

    # HTML 리포트 생성
    html = (
        "<html><head><meta charset='utf-8'><title>RBQM Report</title>"
        "<style>body{font-family:sans-serif;margin:32px;}"
        "pre{background:#f4f4f4;padding:16px;border-radius:6px;"
        "white-space:pre-wrap;}h1{color:#2c3e50;}"
        ".disc{background:#fff3cd;padding:12px;border-radius:6px;}</style></head>"
        "<body><h1>MASLDTrialReadQC-Kor — RBQM 리포트</h1>"
        "<div class='disc'>본 리포트는 연구용·참고용입니다. 실제 임상시험 "
        "모니터링 의사결정은 자격을 갖춘 담당자가 수행해야 합니다. 수치 기준은 "
        "예시·교육용이며 최신 가이드라인으로 검증해야 합니다.</div>"
        f"<pre>{report_text}</pre></body></html>"
    )

    cdl1, cdl2 = st.columns(2)
    cdl1.download_button(
        "텍스트 리포트 다운로드 (.txt)",
        data=report_text.encode("utf-8"),
        file_name="rbqm_report.txt",
        mime="text/plain",
    )
    cdl2.download_button(
        "HTML 리포트 다운로드 (.html)",
        data=html.encode("utf-8"),
        file_name="rbqm_report.html",
        mime="text/html",
    )

    st.caption(
        "참고: ICH E6(R2)/E6(R3) · FDA 2013 Risk-Based Monitoring guidance · "
        "AASLD MASLD 2023 · EASL-EASD-EASO MASLD 2024."
    )
