"""Reporting utilities: plots, tables, GraphPad export, reproducibility checklist.

All output is offline.  No network calls.
"""
from __future__ import annotations

import csv
import math
import os
from datetime import datetime
from typing import Any, Dict, List, Optional

import numpy as np

import matplotlib

matplotlib.use("Agg")  # headless
import matplotlib.pyplot as plt  # noqa: E402


# ---------------------------------------------------------------------------
# Plots
# ---------------------------------------------------------------------------
def plot_spaghetti(
    time_min: List[float],
    channels: Dict[str, List[float]],
    title: str,
    out_png: str,
    ylabel: str = "Insulin (ng/mL)",
) -> None:
    """Per-condition spaghetti curves + median +- IQR overlay."""
    fig, ax = plt.subplots(figsize=(8, 5))
    arrays = []
    for ch, v in channels.items():
        ax.plot(time_min, v, alpha=0.5, linewidth=1.0, label=ch)
        arrays.append(np.array(v, dtype=float))

    if arrays:
        mat = np.vstack(arrays)
        med = np.nanmedian(mat, axis=0)
        q1 = np.nanpercentile(mat, 25, axis=0)
        q3 = np.nanpercentile(mat, 75, axis=0)
        ax.plot(time_min, med, color="black", linewidth=2.5, label="median")
        ax.fill_between(time_min, q1, q3, color="gray", alpha=0.25, label="IQR")

    # stimulus shading
    ax.axvspan(10, 40, color="#ffebcc", alpha=0.5, label="16.7 mM glucose")
    ax.axvspan(40, 50, color="#cce5ff", alpha=0.5, label="30 mM KCl")
    ax.set_xlabel("Time (min)")
    ax.set_ylabel(ylabel)
    ax.set_title(title)
    ax.legend(loc="upper right", fontsize=7, ncol=2)
    ax.grid(alpha=0.2)
    fig.tight_layout()
    fig.savefig(out_png, dpi=140)
    plt.close(fig)


def plot_multi_analyte(
    time_min: List[float],
    analyte_traces: Dict[str, Dict[str, List[float]]],
    channel: str,
    out_png: str,
) -> None:
    """One channel, all analytes overlaid (twin axes)."""
    fig, ax = plt.subplots(figsize=(8, 5))
    ax2 = ax.twinx()
    colors = {"insulin": "#cc4444", "c-peptide": "#4477cc", "glucagon": "#44aa44", "proinsulin": "#aa66aa"}
    for analyte, ch_dict in analyte_traces.items():
        if channel not in ch_dict:
            continue
        target = ax2 if analyte == "glucagon" else ax
        target.plot(time_min, ch_dict[channel], label=analyte, color=colors.get(analyte, "black"))
    ax.axvspan(10, 40, color="#ffebcc", alpha=0.4)
    ax.axvspan(40, 50, color="#cce5ff", alpha=0.4)
    ax.set_xlabel("Time (min)")
    ax.set_ylabel("Insulin / C-pep / Proinsulin (ng/mL)")
    ax2.set_ylabel("Glucagon (pg/mL)")
    ax.set_title(f"Multi-analyte trace: {channel}")
    h1, l1 = ax.get_legend_handles_labels()
    h2, l2 = ax2.get_legend_handles_labels()
    ax.legend(h1 + h2, l1 + l2, loc="upper right", fontsize=8)
    ax.grid(alpha=0.2)
    fig.tight_layout()
    fig.savefig(out_png, dpi=140)
    plt.close(fig)


# ---------------------------------------------------------------------------
# Kinetic parameter table
# ---------------------------------------------------------------------------
def write_kinetic_csv(rows: List[Dict[str, Any]], out_csv: str) -> None:
    if not rows:
        return
    keys = list(rows[0].keys())
    with open(out_csv, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=keys)
        w.writeheader()
        for r in rows:
            w.writerow(r)


def write_kinetic_xlsx(rows: List[Dict[str, Any]], out_xlsx: str) -> bool:
    """Write XLSX if openpyxl available; else skip and return False."""
    try:
        from openpyxl import Workbook  # type: ignore
    except Exception:
        return False
    if not rows:
        return False
    wb = Workbook()
    ws = wb.active
    ws.title = "kinetics"
    keys = list(rows[0].keys())
    ws.append(keys)
    for r in rows:
        ws.append([r.get(k, "") for k in keys])
    wb.save(out_xlsx)
    return True


# ---------------------------------------------------------------------------
# GraphPad-ready CSV export
# ---------------------------------------------------------------------------
def graphpad_export(
    time_min: List[float], channels: Dict[str, List[float]], out_csv: str
) -> None:
    """Wide CSV: column 1 = X (time), columns 2..N = each channel.

    GraphPad Prism imports this directly via 'Import > Plain text or CSV'.
    """
    with open(out_csv, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        ch_names = list(channels.keys())
        w.writerow(["Time_min"] + ch_names)
        for i, t in enumerate(time_min):
            row = [t] + [channels[c][i] if i < len(channels[c]) else "" for c in ch_names]
            w.writerow(row)


def graphpad_pzfx_stub(out_path: str) -> None:
    """Write a minimal Prism .pzfx (XML) stub describing how to import the CSV.

    A true binary .pzf cannot be reproduced offline without the Prism format,
    so we emit a documented .pzfx skeleton that Prism will open and prompt
    the user to map the bundled CSV.
    """
    xml = (
        '<?xml version="1.0" encoding="UTF-8"?>\n'
        '<GraphPadPrismFile PrismXMLVersion="5.00">\n'
        "  <Created><OriginalVersion CreatedByProgram=\"IsletPerifusionAnalyzer\"/></Created>\n"
        "  <InfoSequence><Ref ID=\"Info0\"/></InfoSequence>\n"
        '  <Info ID="Info0">\n'
        "    <Title>Imported perifusion traces</Title>\n"
        "    <Notes>Open kinetics_graphpad.csv via File &gt; Import.</Notes>\n"
        "  </Info>\n"
        "</GraphPadPrismFile>\n"
    )
    with open(out_path, "w", encoding="utf-8") as f:
        f.write(xml)


# ---------------------------------------------------------------------------
# Reproducibility checklist (Diabetes / Diabetologia minimum)
# ---------------------------------------------------------------------------
CHECKLIST_TEMPLATE = [
    ("Sample source", "species, sex, donor ID / strain, age, BMI"),
    ("IEQ / cell number / protein", "report which normalization basis was used"),
    ("Culture conditions", "media, glucose, FBS, days post-isolation"),
    ("Perifusion system", "vendor (BioRep / Brandel / in-house), chamber volume, tubing length"),
    ("Flow rate", "mL/min and stability check (CV < 5%)"),
    ("Dead volume + transit time", "uL and computed lag (min)"),
    ("Stimulation protocol", "glucose steps (mM, min), secretagogues (uM, min)"),
    ("Reference KCl peak", "30 mM, intra-batch CV (target <= 15%)"),
    ("Assay", "ELISA / Luminex / HTRF; LLOD; ULOQ; intra-/inter-assay CV"),
    ("Standard curve", "linear range, R^2, fit method"),
    ("Degradation correction", "storage hours, decay rate applied"),
    ("Baseline subtraction", "pre-stimulus window, mean +- 2SD"),
    ("Data exclusions", "criteria for failed channels"),
    ("Statistical test", "n, replicates, test, multiplicity correction"),
    ("Code + raw data", "DOI / repo for raw CSV + analysis scripts"),
]


def write_checklist_md(out_md: str, ctx: Dict[str, Any]) -> None:
    today = datetime.utcnow().strftime("%Y-%m-%d")
    lines = [
        "# Diabetes / Diabetologia Reproducibility Checklist",
        f"_generated {today} UTC by IsletPerifusionAnalyzer_",
        "",
        "Item | Requirement | Reported value",
        "---- | ----------- | --------------",
    ]
    fill = ctx.get("fill", {})
    for name, req in CHECKLIST_TEMPLATE:
        v = fill.get(name, "[fill in]")
        lines.append(f"{name} | {req} | {v}")
    lines += [
        "",
        "## Auto-detected pass/fail",
        f"- KCl intra-batch CV: **{ctx.get('kcl_cv_pct', 'n/a')}** (threshold <= 15%)",
        f"- KCl normalization gate: **{'PASS' if ctx.get('kcl_pass') else 'FAIL'}**",
        f"- Channels analyzed: {ctx.get('n_channels', 0)}",
        f"- Files parsed: {ctx.get('n_files', 0)}",
        "",
        "## Disclaimer",
        "본 도구는 연구·참고용이며 임상 의사결정에 직접 사용 금지.",
    ]
    with open(out_md, "w", encoding="utf-8") as f:
        f.write("\n".join(lines))


def checklist_to_pdf(md_path: str, pdf_path: str) -> bool:
    """Try WeasyPrint -> matplotlib fallback. Returns True if PDF written."""
    try:
        from weasyprint import HTML  # type: ignore

        with open(md_path, "r", encoding="utf-8") as f:
            md = f.read()
        # naive markdown -> html (avoid extra deps)
        html = md_to_html(md)
        HTML(string=html).write_pdf(pdf_path)
        return True
    except Exception:
        # matplotlib fallback: render text into PDF page.  Korean fonts may not be
        # available; transliterate the only Korean line we emit to ASCII so the
        # rendered PDF stays legible even when DejaVu lacks the glyphs.
        with open(md_path, "r", encoding="utf-8") as f:
            md = f.read()
        ascii_md = md.replace(
            "본 도구는 연구·참고용이며 임상 의사결정에 직접 사용 금지.",
            "[Disclaimer] This tool is for research/reference only; not for direct clinical decision use.",
        )
        from matplotlib.backends.backend_pdf import PdfPages

        import warnings as _w
        with PdfPages(pdf_path) as pdf:
            fig = plt.figure(figsize=(8.27, 11.69))  # A4
            fig.text(
                0.05,
                0.97,
                ascii_md,
                family="monospace",
                fontsize=8,
                verticalalignment="top",
                wrap=True,
            )
            with _w.catch_warnings():
                _w.simplefilter("ignore")
                pdf.savefig(fig)
            plt.close(fig)
        return True


def md_to_html(md: str) -> str:
    """Minimal markdown converter (h1/h2/li/table-ish/text)."""
    out = ["<html><body style='font-family:sans-serif;'>"]
    for line in md.splitlines():
        if line.startswith("# "):
            out.append(f"<h1>{line[2:]}</h1>")
        elif line.startswith("## "):
            out.append(f"<h2>{line[3:]}</h2>")
        elif line.startswith("- "):
            out.append(f"<li>{line[2:]}</li>")
        elif "|" in line and "---" not in line:
            cells = [c.strip() for c in line.split("|")]
            out.append("<tr>" + "".join(f"<td>{c}</td>" for c in cells) + "</tr>")
        else:
            out.append(f"<p>{line}</p>")
    out.append("</body></html>")
    return "\n".join(out)


# ---------------------------------------------------------------------------
# Markdown methods-section draft
# ---------------------------------------------------------------------------
def methods_draft_md(ctx: Dict[str, Any], out_md: str) -> None:
    n_files = ctx.get("n_files", 0)
    n_channels = ctx.get("n_channels", 0)
    flow = ctx.get("flow_rate_ml_min", 0.1)
    dv = ctx.get("dead_volume_ul", 100)
    cv_raw = ctx.get("kcl_cv_pct", float("nan"))
    try:
        cv_str = f"{float(cv_raw):.1f}%"
    except (TypeError, ValueError):
        cv_str = f"{cv_raw}"
    text = (
        "# Methods (auto-draft)\n\n"
        "Islet perifusion was performed using a "
        f"{ctx.get('vendor', 'in-house')} perifusion system at "
        f"{flow:.2f} mL/min with a chamber dead volume of {dv:.0f} µL "
        f"(transit-time lag = {(dv / (flow * 1000)):.2f} min). "
        "Following a 10-min basal period at 2.8 mM glucose, samples were "
        "stimulated sequentially with 16.7 mM glucose (10–40 min), "
        "30 mM KCl (40–50 min), and returned to 2.8 mM glucose (50–60 min). "
        f"{n_files} file(s) and {n_channels} channel(s) were analyzed. "
        "Traces were time-shifted to correct for transit lag, "
        "drift-corrected for storage-time-dependent peptide degradation, "
        "and normalized to IEQ where reported. Inter-batch normalization "
        "used the 30 mM KCl peak with intra-batch CV "
        f"= {cv_str} (target ≤ 15%). 1st-phase response was defined as "
        "10–20 min, 2nd-phase as 20–40 min. AUC was computed by trapezoidal "
        "integration over 0–10, 10–30, and 30–60 min windows.\n\n"
        "**Disclaimer.** 본 도구는 연구·참고용이며 임상 의사결정에 직접 사용 금지.\n"
    )
    with open(out_md, "w", encoding="utf-8") as f:
        f.write(text)
