# -*- coding: utf-8 -*-
"""Utilities for generating Global Quality Index reports."""

import os
import json
import glob
import configparser
from statistics import mean
from typing import Union, Optional, Dict

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.cm as cm

from meg_qc.calculation.initial_meg_qc import get_all_config_params


def _safe_dict(val):
    """Return ``val`` if it is a dictionary, otherwise an empty ``dict``.

    This helper prevents ``AttributeError`` when optional sections of the
    metrics JSON are missing or set to ``null``. It normalises any non mapping
    values to an empty dictionary so subsequent ``.get()`` calls succeed.
    """

    return val if isinstance(val, dict) else {}


def _safe_dataframe(obj):
    """Return a ``DataFrame`` from ``obj`` while tolerating malformed inputs.

    ``obj`` is expected to be a mapping. When it is not, an empty
    ``DataFrame`` is returned.  Some metrics can store scalar dictionaries
    (e.g. results for a single epoch). In that case ``pandas`` raises a
    ``ValueError`` if we try to build a ``DataFrame`` directly.  This helper
    converts such scalar dictionaries into a single-row ``DataFrame`` so the
    rest of the code can operate uniformly.
    """
    if not isinstance(obj, dict):
        return pd.DataFrame()

    try:
        return pd.DataFrame(obj)
    except ValueError:
        # Fall back to single-row construction for scalar dictionaries
        return pd.DataFrame([{k: v for k, v in obj.items()}])


# ---------------------------------------------------------------------------
# High level helper functions
# ---------------------------------------------------------------------------


def create_summary_report(
    json_file: Union[str, os.PathLike],
    html_output: str | None = None,
    json_output: str = "first_sight_report.json",
    gqi_settings: Optional[Dict[str, Dict[str, float]]] = None,
):
    """Create a human readable QC summary from the metrics JSON file.

    Parameters
    ----------
    json_file : Union[str, os.PathLike]
        Path to the ``SimpleMetrics`` JSON file generated by the calculation
        pipeline.
    html_output : str | None
        Optional path where an HTML report will be written. If ``None`` no HTML
        file is produced.
    json_output : str
        Name of the summary JSON file to create.
    gqi_settings : dict | None
        Dictionary with Global Quality Index configuration. When ``None`` the
        default thresholds defined in this function are used.
    """

    # Read the metrics produced by the calculation pipeline
    with open(json_file, "r", encoding="utf-8") as f:
        data = json.load(f)

    # Retrieve nested structures safely in case metrics were not computed
    muscle = _safe_dict(data.get("MUSCLE"))
    psd = _safe_dict(data.get("PSD"))
    psd_global = _safe_dict(psd.get("PSD_global"))

    # Number of events used when computing the muscle metric
    total_events = muscle.get("total_number_of_events")

    # Extract PSD noise information for magnetometers and gradiometers
    psd_details_mag = _safe_dict(_safe_dict(psd_global.get("mag")).get("details"))
    psd_details_grad = _safe_dict(_safe_dict(psd_global.get("grad")).get("details"))
    # Percentage of power attributed to noise for each sensor type
    noisy_power_mag = sum(
        d.get("percent_of_this_noise_ampl_relative_to_all_signal_global", 0)
        for d in psd_details_mag.values()
    )
    noisy_power_grad = sum(
        d.get("percent_of_this_noise_ampl_relative_to_all_signal_global", 0)
        for d in psd_details_grad.values()
    )
    # Average noise level across available sensor types
    if psd_details_mag and psd_details_grad:
        M_psd = mean([noisy_power_mag, noisy_power_grad])
    elif psd_details_mag:
        M_psd = noisy_power_mag
    elif psd_details_grad:
        M_psd = noisy_power_grad
    else:
        M_psd = None

    # Determine which metrics are available
    std_present = bool(data.get("STD"))
    psd_present = bool(data.get("PSD"))
    ptp_present = bool(data.get("PTP_MANUAL"))
    muscle_present = bool(data.get("MUSCLE"))
    ecg_present = bool(data.get("ECG"))
    eog_present = bool(data.get("EOG"))

    compute_gqi = True
    include_corr = True
    if gqi_settings is not None:
        # Override default behaviour when configuration is provided
        compute_gqi = gqi_settings.get("compute_gqi", True)
        include_corr = gqi_settings.get("include_ecg_eog", True)

    # If correlation metrics are missing treat it as disabled
    if not (ecg_present and eog_present):
        include_corr = False

    # Only compute GQI when mandatory metrics are available
    compute_gqi = (
        compute_gqi
        and std_present
        and psd_present
        and ptp_present
    )

    if html_output is not None:
        html_name = os.path.splitext(os.path.basename(json_output))[0].replace(
            "-GlobalSummaryReport_meg",
            "",
        )

    def build_summary_table(source):
        """Return a table summarising noisy and flat channels."""
        rows = []
        for sensor_type in ["mag", "grad"]:
            n_noisy = source[sensor_type]["number_of_noisy_ch"]
            p_noisy = source[sensor_type]["percent_of_noisy_ch"]
            n_flat = source[sensor_type]["number_of_flat_ch"]
            p_flat = source[sensor_type]["percent_of_flat_ch"]
            rows.append({"Metric": "Noisy Channels", sensor_type: f"{n_noisy} ({p_noisy:.1f}%)"})
            rows.append({"Metric": "Flat Channels", sensor_type: f"{n_flat} ({p_flat:.1f}%)"})
        df = pd.DataFrame(rows)
        df = df.groupby("Metric").first().reset_index()
        df.rename(columns={"mag": "MAGNETOMETERS", "grad": "GRADIOMETERS"}, inplace=True)
        return df

    if std_present:
        general_df = build_summary_table(data["STD"]["STD_all_time_series"])
        std_epoch_df = _safe_dataframe(data["STD"].get("STD_epoch", {}))
        std_lvl = data["STD"]["STD_all_time_series"]["mag"].get("std_lvl", "NA")
        # ``STD_epoch`` can be ``null`` in the metrics JSON. Applying ``_safe_dict``
        # twice ensures we always operate on a dictionary before requesting the
        # magnetometer multiplier.
        std_epoch_lvl = (
            _safe_dict(
                _safe_dict(
                    _safe_dict(data.get("STD", {})).get("STD_epoch")
                ).get("mag")
            ).get("noisy_channel_multiplier", "NA")
        )
    else:
        general_df = pd.DataFrame()
        std_epoch_df = pd.DataFrame()
        std_lvl = "NA"
        std_epoch_lvl = "NA"

    if ptp_present:
        ptp_df = build_summary_table(data["PTP_MANUAL"]["ptp_manual_all"])
        ptp_epoch_df = _safe_dataframe(data["PTP_MANUAL"].get("ptp_manual_epoch", {}))
        ptp_lvl = data["PTP_MANUAL"]["ptp_manual_all"]["mag"].get("ptp_lvl", "NA")
        # ``ptp_manual_epoch`` can also be ``null``. By wrapping the nested
        # ``get`` calls with ``_safe_dict`` we ensure a default dictionary and
        # avoid ``AttributeError`` when extracting the multiplier.
        ptp_epoch_lvl = (
            _safe_dict(
                _safe_dict(
                    _safe_dict(data.get("PTP_MANUAL", {})).get("ptp_manual_epoch")
                ).get("mag")
            ).get("noisy_channel_multiplier", "NA")
        )
    else:
        ptp_df = pd.DataFrame()
        ptp_epoch_df = pd.DataFrame()
        ptp_lvl = "NA"
        ptp_epoch_lvl = "NA"

    def build_psd_summary(noise_mag, noise_grad):
        """Return a table with global PSD noise percentages."""
        df = pd.DataFrame([
            {"Metric": "Noise Power", "mag": noise_mag, "grad": noise_grad}
        ])
        df["mag"] = df["mag"].map(lambda v: f"{v:.2f}%")
        df["grad"] = df["grad"].map(lambda v: f"{v:.2f}%")
        df.rename(columns={"mag": "MAGNETOMETERS", "grad": "GRADIOMETERS"}, inplace=True)
        return df

    psd_df = build_psd_summary(noisy_power_mag, noisy_power_grad)

    # Default thresholds and weights for the GQI formula
    thresholds = {
        "ch": {"start": 5.0, "end": 30.0, "weight": 0.32},
        "corr": {"start": 5.0, "end": 25.0, "weight": 0.24},
        "mus": {"start": 1.0, "end": 10.0, "weight": 0.24},
        "psd": {"start": 1.0, "end": 5.0, "weight": 0.2},
    }
    if gqi_settings is not None:
        # Override defaults with user supplied thresholds when available
        for key, val in gqi_settings.items():
            if isinstance(val, dict):
                thresholds[key] = {**thresholds.get(key, {}), **val}

    def quality_q(M, start, end):
        """Linear quality value between ``start`` and ``end`` thresholds."""
        if M <= start:
            return 1.0
        if M >= end:
            return 0.0
        f = (M - start) / (end - start)
        return 1.0 - f

    def count_high_correlations_from_details(section, contamination_key):
        """Return a table with channels having |corr| > 0.8 for ECG/EOG."""
        results = []
        percentages = []
        for sensor_type in ["mag", "grad"]:
            entries = data.get(section)
            entries = _safe_dict(entries)
            entries = _safe_dict(entries.get(contamination_key))
            entries = _safe_dict(entries.get(sensor_type))
            entries = _safe_dict(entries.get("details"))
            total = len(entries)
            high_corr = sum(
                1
                for _, pair in entries.items()
                if isinstance(pair, (list, tuple)) and pair and abs(pair[0]) > 0.8
            )
            percent = 100 * high_corr / total if total > 0 else 0
            percentages.append(percent)
            results.append(
                {
                    "Sensor Type": "MAGNETOMETERS" if sensor_type == "mag" else "GRADIOMETERS",
                    "# |High Correlations| > 0.8": f"{high_corr} ({percent:.1f}%)",
                    "Total Channels": total,
                }
            )
        return pd.DataFrame(results), percentages

    # Summaries of ECG and EOG channel correlations
    if ecg_present:
        ecg_df, ecg_percents = count_high_correlations_from_details(
            "ECG", "all_channels_ranked_by_ECG_contamination_level"
        )
    else:
        ecg_df, ecg_percents = pd.DataFrame(), []

    if eog_present:
        eog_df, eog_percents = count_high_correlations_from_details(
            "EOG", "all_channels_ranked_by_EOG_contamination_level"
        )
    else:
        eog_df, eog_percents = pd.DataFrame(), []

    def is_noisy(desc: str) -> bool:
        """Return ``True`` if the description indicates a noisy channel."""
        noisy_markers = ["too noisy", "does not have expected", "can not be detected"]
        return any(m in desc for m in noisy_markers)

    ecg_desc = str(_safe_dict(data.get("ECG")).get("description", "")).lower()
    eog_desc = str(_safe_dict(data.get("EOG")).get("description", "")).lower()
    ecg_noisy = is_noisy(ecg_desc)
    eog_noisy = is_noisy(eog_desc)

    # Average percentage of bad (noisy or flat) channels for STD and PTP
    if std_present:
        std_global = _safe_dict(data["STD"].get("STD_all_time_series"))
        vals = []
        mag = _safe_dict(std_global.get("mag"))
        grad = _safe_dict(std_global.get("grad"))
        if mag:
            vals.append(mag.get("percent_of_noisy_ch"))
            vals.append(mag.get("percent_of_flat_ch"))
        if grad:
            vals.append(grad.get("percent_of_noisy_ch"))
            vals.append(grad.get("percent_of_flat_ch"))
        vals = [v for v in vals if v is not None]
        std_pct = mean(vals) if vals else None
    else:
        std_pct = None

    if ptp_present:
        ptp_global = _safe_dict(data["PTP_MANUAL"].get("ptp_manual_all"))
        vals = []
        mag = _safe_dict(ptp_global.get("mag"))
        grad = _safe_dict(ptp_global.get("grad"))
        if mag:
            vals.append(mag.get("percent_of_noisy_ch"))
            vals.append(mag.get("percent_of_flat_ch"))
        if grad:
            vals.append(grad.get("percent_of_noisy_ch"))
            vals.append(grad.get("percent_of_flat_ch"))
        vals = [v for v in vals if v is not None]
        ptp_pct = mean(vals) if vals else None
    else:
        ptp_pct = None

    vals = [v for v in (std_pct, ptp_pct) if v is not None]
    bad_pct = mean(vals) if vals else None

    def mean_or_end(percs):
        """Return mean of list or ``None`` when empty."""
        vals = [p for p in percs if p is not None]
        return mean(vals) if vals else None

    ecg_pct = mean_or_end(ecg_percents)
    eog_pct = mean_or_end(eog_percents)

    # Correlation weight is split equally between ECG and EOG
    weight_corr_each = thresholds["corr"]["weight"] / 2
    weight_ecg = weight_corr_each if include_corr and ecg_present else 0.0
    weight_eog = weight_corr_each if include_corr and eog_present else 0.0

    # Quality score for ECG and EOG correlations
    q_corr_ecg = (
        0.5 if ecg_noisy else quality_q(ecg_pct, thresholds["corr"]["start"], thresholds["corr"]["end"])
        if weight_ecg
        else 1.0
    )
    q_corr_eog = (
        0.5 if eog_noisy else quality_q(eog_pct, thresholds["corr"]["start"], thresholds["corr"]["end"])
        if weight_eog
        else 1.0
    )

    muscle_events = _safe_dict(muscle.get("zscore_thresholds")).get("number_muscle_events")
    muscle_pct = None
    if muscle_events is not None and total_events is not None:
        muscle_pct = 100.0 * muscle_events / total_events if total_events else float(muscle_events)

    if bad_pct is not None:
        q_ch = quality_q(bad_pct, thresholds["ch"]["start"], thresholds["ch"]["end"])
    else:
        q_ch = 1.0

    if muscle_pct is not None:
        q_mus = quality_q(muscle_pct, thresholds["mus"]["start"], thresholds["mus"]["end"])
    else:
        q_mus = 1.0

    if M_psd is not None:
        q_psd = quality_q(M_psd, thresholds["psd"]["start"], thresholds["psd"]["end"])
    else:
        q_psd = 1.0

    # Sum of weights used for normalising the GQI calculation
    weights_sum = (
        thresholds["ch"]["weight"] + weight_ecg + weight_eog + thresholds["mus"]["weight"] + thresholds["psd"]["weight"]
    )
    if weights_sum == 0:
        weights_sum = 1.0

    if compute_gqi:
        # Weighted combination of quality scores scaled to 0-100
        GQI = round(
            100.0
            * (
                thresholds["ch"]["weight"] * q_ch
                + weight_ecg * q_corr_ecg
                + weight_eog * q_corr_eog
                + thresholds["mus"]["weight"] * q_mus
                + thresholds["psd"]["weight"] * q_psd
            )
            / weights_sum,
            2,
        )
        # Penalties reflect the individual contribution of each metric
        penalties = {
            "ch": 100 * thresholds["ch"]["weight"] * (1 - q_ch) / weights_sum,
            "corr": 100 * (weight_ecg * (1 - q_corr_ecg) + weight_eog * (1 - q_corr_eog)) / weights_sum,
            "mus": 100 * thresholds["mus"]["weight"] * (1 - q_mus) / weights_sum,
            "psd": 100 * thresholds["psd"]["weight"] * (1 - q_psd) / weights_sum,
        }
    else:
        GQI = None
        penalties = {}

    muscle_df = pd.DataFrame([
        {"# Muscle Events": muscle_events, "Total Events": total_events if total_events is not None else 0}
    ])

    if html_output is not None:
        style = """
            <style>
                body { font-family: Arial, sans-serif; margin: 10px; font-size: 16px; }
                h1 { color: #003366; font-size: 25px; margin-bottom: 6px; font-weight: bold; }
                h2 { color: #004d99; font-size: 19px; margin: 12px 0 6px 0; }
                table { border-collapse: collapse; margin: 0 0 8px 0; font-size: 16px; }
                th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: center; }
                th { background-color: #f2f2f2; }
                .table-flex { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-start; margin-bottom: 12px; }
                .table-box { flex: 1; min-width: 300px; }
                .file-label { font-size: 18px; font-weight: bold; margin: 0 0 2px 12px; }
                .subtitle { font-size: 19px; font-weight: bold; color: #222; margin: 0 0 12px 12px; }
                .header-grid { display: grid; grid-template-columns: 1fr 1fr; align-items: start; margin-bottom: 0; }
            </style>
        """
        with open(html_output, "w", encoding="utf-8") as f:
            f.write("</div></div>")
            f.write("<html><head><meta charset='UTF-8'>" + style + "</head><body>")
            f.write("<div class='header-grid'>")
            f.write("<div><h1>MEGQC Global Quality Report</h1></div>")
            f.write(f"<div><div class='file-label'>File: {html_name}</div>")
            f.write(f"<div class='subtitle'>Global Quality Index (GQI): {GQI}</div></div></div>")
            if penalties:
                f.write("<h2>GQI Penalties</h2>")
                f.write(
                    pd.DataFrame([
                        {"Metric": "Bad Channels", "Penalty (%)": f"{penalties['ch']:.2f}"},
                        {"Metric": "Correlation", "Penalty (%)": f"{penalties['corr']:.2f}"},
                        {"Metric": "Muscle", "Penalty (%)": f"{penalties['mus']:.2f}"},
                        {"Metric": "PSD Noise", "Penalty (%)": f"{penalties['psd']:.2f}"},
                    ]).to_html(index=False)
                )
            f.write("<div class='table-flex'>")
            f.write(f"<div class='table-box'><h2>STD Time-Series (STD level: {std_lvl})</h2>")
            f.write(general_df.to_html(index=False))
            f.write(f"</div><div class='table-box'><h2>PTP Time-Series (STD level: {ptp_lvl})</h2>")
            f.write(ptp_df.to_html(index=False))
            f.write("</div></div>")
            f.write("<h2>PSD Noise Summary</h2>")
            f.write(psd_df.to_html(index=False))
            f.write("<div class='table-flex'>")
            f.write(f"<div class='table-box'><h2>STD Epoch Summary (STD level: {std_epoch_lvl})</h2>")
            f.write(std_epoch_df.to_html(index=False))
            f.write(f"</div><div class='table-box'><h2>PTP Epoch Summary (STD level: {ptp_epoch_lvl})</h2>")
            f.write(ptp_epoch_df.to_html(index=False))
            f.write("</div></div>")
            if not ecg_df.empty or not eog_df.empty:
                f.write("<div class='table-flex'>")
                if not ecg_df.empty:
                    f.write("<div class='table-box'><h2>ECG Correlation Summary</h2>")
                    f.write(ecg_df.to_html(index=False))
                    f.write("</div>")
                if not eog_df.empty:
                    f.write("<div class='table-box'><h2>EOG Correlation Summary</h2>")
                    f.write(eog_df.to_html(index=False))
                    f.write("</div>")
                f.write("</div>")
            f.write("<h2>Muscle Events Summary</h2>")
            f.write(muscle_df.to_html(index=False))
            f.write("</body></html>")

    # Collate all results into a JSON-friendly structure
    summary_data = {
        "file_name": os.path.basename(json_output),
        "GQI": GQI,
        "STD_time_series": general_df.to_dict(orient="records"),
        "PTP_time_series": ptp_df.to_dict(orient="records"),
        "STD_epoch_summary": std_epoch_df.to_dict(orient="records"),
        "PTP_epoch_summary": ptp_epoch_df.to_dict(orient="records"),
        "ECG_correlation_summary": ecg_df.to_dict(orient="records"),
        "EOG_correlation_summary": eog_df.to_dict(orient="records"),
        "PSD_noise_summary": psd_df.to_dict(orient="records"),
        "Muscle_events": {"# Muscle Events": muscle_events, "total_number_of_events": total_events},
        "GQI_penalties": penalties,
        "GQI_metrics": {
            "bad_pct": bad_pct,
            "std_pct": std_pct,
            "ptp_pct": ptp_pct,
            "ecg_pct": ecg_pct,
            "eog_pct": eog_pct,
            "muscle_pct": muscle_pct,
            "psd_noise_pct": M_psd,
        },
        "parameters": {
            "std_lvl": std_lvl,
            "ptp_lvl": ptp_lvl,
            "std_epoch_lvl": std_epoch_lvl,
            "ptp_epoch_lvl": ptp_epoch_lvl,
        },
    }

    with open(json_output, "w", encoding="utf-8") as f_json:
        json.dump(summary_data, f_json, indent=4)

    # Inform the user about generated outputs
    print(f"HTML successfully generated: {html_output}")
    print(f"JSON summary successfully generated: {json_output}")


def create_group_metrics_figure(tsv_path: Union[str, os.PathLike], output_png: Union[str, os.PathLike]) -> None:
    """Generate violin plot of group GQI metrics."""
    # Read the table with per-subject metrics
    df = pd.read_csv(tsv_path, sep="\t")

    cols = [
        "GQI",
        "GQI_penalty_ch",
        "GQI_penalty_corr",
        "GQI_penalty_mus",
        "GQI_penalty_psd",
        "GQI_std_pct",
        "GQI_ptp_pct",
        "GQI_ecg_pct",
        "GQI_eog_pct",
        "GQI_muscle_pct",
        "GQI_psd_noise_pct",
    ]

    label_map = {
        "GQI": "GQI",
        "GQI_penalty_ch": "Variability penalty",
        "GQI_penalty_corr": "Correlational penalty",
        "GQI_penalty_mus": "Muscle penalty",
        "GQI_penalty_psd": "PSD penalty",
        "GQI_std_pct": "STD noise",
        "GQI_ptp_pct": "PtP noise",
        "GQI_ecg_pct": "ECG noise",
        "GQI_eog_pct": "EOG noise",
        "GQI_muscle_pct": "Muscle noise",
        "GQI_psd_noise_pct": "PSD noise",
    }
    # Only plot metrics present in the table and containing data
    available_cols = [
        c for c in cols if c in df.columns and not df[c].dropna().empty
    ]
    data = df[available_cols].apply(pd.to_numeric, errors="coerce")
    violin_data = [data[c].dropna().values for c in available_cols]

    palette = cm.get_cmap("tab10", len(available_cols))

    plt.figure(figsize=(22, 10))
    # Draw the violin plot with mean lines
    parts = plt.violinplot(
        violin_data,
        showmeans=True,
        showextrema=True,
        showmedians=False,
        widths=0.8,
    )

    # Colour each violin body for clarity
    for i, pc in enumerate(parts["bodies"]):
        color = palette(i)
        pc.set_facecolor(color)
        pc.set_edgecolor("black")
        pc.set_alpha(0.3)

    parts["cmeans"].set_linewidth(3)
    parts["cmeans"].set_color("black")
    parts["cbars"].set_color("black")

    # Overlay individual data points with jitter
    for i, y in enumerate(violin_data, start=1):
        x = np.random.normal(i, 0.08, size=len(y))
        plt.scatter(
            x,
            y,
            s=40,
            alpha=0.3,
            edgecolor="black",
            linewidth=0.6,
            facecolor=palette(i - 1),
        )

    # Label axes and finalise the figure
    tick_labels = [label_map.get(c, c) for c in available_cols]
    plt.xticks(
        range(1, len(available_cols) + 1),
        tick_labels,
        rotation=35,
        ha="right",
        fontsize=20,
        fontweight="bold",
    )
    plt.yticks(fontsize=18)
    plt.ylabel("Percentage of Quality, Penalties and Noise", fontsize=22, fontweight="bold")
    plt.title("Violin Plot of GQI Metrics with Individual Data Points", fontsize=26, pad=25)

    plt.grid(axis="y", linestyle="--", alpha=0.3)
    plt.tight_layout()
    plt.savefig(output_png, dpi=300)
    plt.close()



def generate_gqi_summary(dataset_path: str, config_file: str) -> None:
    """Generate Global Quality Index summaries from existing metrics."""
    # Load user configuration to retrieve GQI settings
    qc_params = get_all_config_params(config_file)
    if qc_params is None:
        return
    gqi_params = qc_params.get("GlobalQualityIndex")

    calc_dir = os.path.join(dataset_path, "derivatives", "Meg_QC", "calculation")
    reports_root = os.path.join(dataset_path, "derivatives", "Meg_QC", "summary_reports")
    os.makedirs(reports_root, exist_ok=True)

    # Create a new attempt folder using incremental numbering
    existing = glob.glob(os.path.join(reports_root, "global_quality_index_*"))
    numbers = [int(os.path.basename(p).split("_")[-1]) for p in existing if os.path.basename(p).split("_")[-1].isdigit()]
    attempt = max(numbers) + 1 if numbers else 1

    attempt_dir = os.path.join(reports_root, f"global_quality_index_{attempt}")
    os.makedirs(attempt_dir, exist_ok=True)

    pattern = os.path.join(calc_dir, "sub-*", "*SimpleMetrics_meg.json")
    summary_paths = []
    for json_path in glob.glob(pattern):
        sub_dir = os.path.basename(os.path.dirname(json_path))
        out_sub = os.path.join(attempt_dir, sub_dir)
        os.makedirs(out_sub, exist_ok=True)
        base = os.path.basename(json_path).replace("SimpleMetrics", f"GlobalSummaryReport_attempt{attempt}")
        out_json = os.path.join(out_sub, base)
        # Generate per-subject summary JSON (no HTML)
        create_summary_report(json_path, None, out_json, gqi_params)
        summary_paths.append(out_json)

    # Collate per-subject summaries into a group table and figure
    group_dir = os.path.join(reports_root, "group_metrics")
    os.makedirs(group_dir, exist_ok=True)
    from meg_qc.calculation.meg_qc_pipeline import flatten_summary_metrics
    rows = []
    for path in summary_paths:
        # Flatten each summary JSON into a one-row dictionary
        with open(path, "r", encoding="utf-8") as f:
            js = json.load(f)
        subject = os.path.basename(os.path.dirname(path))
        row = {"subject": subject}
        row.update(flatten_summary_metrics(js))
        rows.append(row)
    if rows:
        df = pd.DataFrame(rows)
        tsv_file = os.path.join(group_dir, f"Global_Quality_Index_attempt_{attempt}.tsv")
        df.to_csv(tsv_file, sep="\t", index=False)
        png_file = os.path.join(group_dir, f"Global_Quality_Index_attempt_{attempt}.png")
        # Produce violin plot summarising group metrics
        create_group_metrics_figure(tsv_file, png_file)

    # Save configuration used for this attempt
    config_dir = os.path.join(reports_root, "config")
    os.makedirs(config_dir, exist_ok=True)
    cfg = configparser.ConfigParser()
    src = configparser.ConfigParser()
    src.read(config_file)
    if src.has_section("GlobalQualityIndex"):
        cfg["GlobalQualityIndex"] = src["GlobalQualityIndex"]
    # Store the configuration used for reproducibility
    with open(os.path.join(config_dir, f"global_quality_index_{attempt}.ini"), "w") as f:
        cfg.write(f)

    print(f"Attempt {attempt} completed. Reports saved to {attempt_dir}")
