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

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

import pandas as pd

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()}])


def _format_count_percent(count: Optional[float], percent: Optional[float]) -> str:
    """Return a human readable string for channel counts and percentages.

    When either ``count`` or ``percent`` is missing the function returns
    ``"NA"`` so that tables keep their column layout even for systems that do
    not provide both sensor types (e.g. CTF which only has gradiometers).
    """

    if count is None and percent is None:
        return "NA"
    if count is None:
        return f"NA ({percent:.1f}%)" if percent is not None else "NA"
    if percent is None:
        return str(count)
    return f"{count} ({percent:.1f}%)"


def _extract_task_label(path: str) -> str:
    """Return a BIDS task label, augmented with run when present.

    Filenames produced by the MEG QC pipeline follow the BIDS convention and
    often include ``task-<label>`` and optional ``run-<idx>`` entities. This
    helper keeps the label concise while still distinguishing repeated runs of
    the same task.
    """
    base = os.path.basename(path)
    task_match = re.search(r"(?<=_task-)[^_]+", base)
    if not task_match:
        return ""
    task = task_match.group(0)

    run_match = re.search(r"(?<=_run-)[^_]+", base)
    if not run_match:
        return task
    return f"{task} (run-{run_match.group(0)})"


def _get_sensor_param(section: dict, subsection: str, param: str, preferred: str = "mag", fallback: str = "grad"):
    """Return a sensor-specific parameter preferring magnetometers when present.

    Elekta datasets expose both magnetometer and gradiometer entries. CTF data
    only provides gradiometers, so we fall back to ``fallback`` when the
    ``preferred`` sensor entry is missing. Returning ``"NA"`` ensures that the
    summary table retains the same column layout for concatenation across
    systems.
    """

    container = _safe_dict(_safe_dict(section).get(subsection))
    preferred_section = _safe_dict(container.get(preferred))
    if preferred_section and preferred_section.get(param) is not None:
        return preferred_section.get(param)

    fallback_section = _safe_dict(container.get(fallback))
    if fallback_section and fallback_section.get(param) is not None:
        return fallback_section.get(param)

    return "NA"


# ---------------------------------------------------------------------------
# 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"))

    has_mag_psd = bool(psd_details_mag)
    has_grad_psd = bool(psd_details_grad)

    # 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()
        )
        if has_mag_psd
        else None
    )
    noisy_power_grad = (
        sum(
            d.get("percent_of_this_noise_ampl_relative_to_all_signal_global", 0)
            for d in psd_details_grad.values()
        )
        if has_grad_psd
        else None
    )

    # Average noise level across available sensor types. For CTF data only the
    # gradiometer branch is available, but Elekta data (mag+grad) keeps the
    # original averaging behaviour unchanged.
    if has_mag_psd and has_grad_psd:
        M_psd = mean([noisy_power_mag, noisy_power_grad])
    elif has_mag_psd:
        M_psd = noisy_power_mag
    elif has_grad_psd:
        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"]:
            data_for_sensor = _safe_dict(_safe_dict(source).get(sensor_type))
            n_noisy = data_for_sensor.get("number_of_noisy_ch")
            p_noisy = data_for_sensor.get("percent_of_noisy_ch")
            n_flat = data_for_sensor.get("number_of_flat_ch")
            p_flat = data_for_sensor.get("percent_of_flat_ch")
            rows.append({"Metric": "Noisy Channels", sensor_type: _format_count_percent(n_noisy, p_noisy)})
            rows.append({"Metric": "Flat Channels", sensor_type: _format_count_percent(n_flat, p_flat)})
        df = pd.DataFrame(rows)
        df = df.groupby("Metric").first().reset_index()
        df.rename(columns={"mag": "MAGNETOMETERS", "grad": "GRADIOMETERS"}, inplace=True)
        return df

    def build_epoch_summary(source):
        """Return a table summarising noisy and flat epochs."""
        def _resolve_epoch_totals(data_for_sensor):
            total_noisy = data_for_sensor.get("total_num_noisy_ep")
            total_noisy_pct = data_for_sensor.get("total_perc_noisy_ep")
            total_flat = data_for_sensor.get("total_num_flat_ep")
            total_flat_pct = data_for_sensor.get("total_perc_flat_ep")

            details = data_for_sensor.get("details")
            if isinstance(details, list) and details:
                total_epochs = len(details)
                noisy_count = sum(1 for item in details if item.get("epoch_too_noisy") is True)
                flat_count = sum(1 for item in details if item.get("epoch_too_flat") is True)
                if total_noisy is None:
                    total_noisy = noisy_count
                if total_flat is None:
                    total_flat = flat_count
                if total_noisy_pct is None:
                    total_noisy_pct = round(noisy_count / total_epochs * 100, 1)
                if total_flat_pct is None:
                    total_flat_pct = round(flat_count / total_epochs * 100, 1)

            return total_noisy, total_noisy_pct, total_flat, total_flat_pct

        rows = []
        for sensor_type in ["mag", "grad"]:
            data_for_sensor = _safe_dict(_safe_dict(source).get(sensor_type))
            total_noisy, total_noisy_pct, total_flat, total_flat_pct = _resolve_epoch_totals(data_for_sensor)
            rows.append(
                {
                    "Sensor Type": "MAGNETOMETERS" if sensor_type == "mag" else "GRADIOMETERS",
                    "Noisy Epochs": _format_count_percent(
                        total_noisy,
                        total_noisy_pct,
                    ),
                    "Flat Epochs": _format_count_percent(
                        total_flat,
                        total_flat_pct,
                    ),
                }
            )
        return pd.DataFrame(rows)

    if std_present:
        general_df = build_summary_table(data["STD"]["STD_all_time_series"])
        std_epoch_df = build_epoch_summary(_safe_dict(data["STD"].get("STD_epoch", {})))
        std_lvl = _get_sensor_param(data.get("STD", {}), "STD_all_time_series", "std_lvl")
        # ``STD_epoch`` can be ``null`` in the metrics JSON. Applying ``_safe_dict``
        # twice ensures we always operate on a dictionary before requesting the
        # sensor multiplier. For CTF data the value is taken from gradiometers.
        std_epoch_lvl = _get_sensor_param(
            data.get("STD", {}), "STD_epoch", "noisy_channel_multiplier"
        )
    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 = build_epoch_summary(_safe_dict(data["PTP_MANUAL"].get("ptp_manual_epoch", {})))
        ptp_lvl = _get_sensor_param(data.get("PTP_MANUAL", {}), "ptp_manual_all", "ptp_lvl")
        # ``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. For CTF data
        # the gradiometer entry is used instead of failing on the missing mag.
        ptp_epoch_lvl = _get_sensor_param(
            data.get("PTP_MANUAL", {}), "ptp_manual_epoch", "noisy_channel_multiplier"
        )
    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}
        ])

        def _fmt_noise(val):
            return f"{val:.2f}%" if val is not None else "NA"

        df["mag"] = df["mag"].map(_fmt_noise)
        df["grad"] = df["grad"].map(_fmt_noise)
        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 generate_gqi_summary(dataset_path: str, megqc_root: str, config_file: str) -> None:
    """Generate Global Quality Index summaries from existing metrics.

    Parameters
    ----------
    dataset_path
        BIDS dataset root (currently used for logging/context only).
    megqc_root
        Root directory of one MEGqc analysis space. This is normally either:
        - ``.../derivatives/Meg_QC`` (legacy mode), or
        - ``.../derivatives/Meg_QC/profiles/<analysis_id>`` (profile mode).

        For backward compatibility, callers may still pass a derivatives root
        (``.../derivatives``). In that case the function auto-resolves
        ``.../derivatives/Meg_QC``.
    config_file
        Settings INI path used to read ``[GlobalQualityIndex]`` parameters.
    """
    # 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")

    # Accept both direct Meg_QC roots and legacy derivatives roots.
    root = os.path.abspath(megqc_root)
    if os.path.isdir(os.path.join(root, "Meg_QC")):
        root = os.path.join(root, "Meg_QC")

    calc_dir = os.path.join(root, "calculation")
    reports_root = os.path.join(root, "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)
        task_label = _extract_task_label(json_path)
        summary_paths.append((out_json, task_label))

    # Collate per-subject summaries into a group table
    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, task 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 = {"task": task, "subject": subject}
        row.update(flatten_summary_metrics(js))
        rows.append(row)
    if rows:
        df = pd.DataFrame(rows)
        # Ensure the task column is the first column of the table
        cols = df.columns.tolist()
        cols.insert(0, cols.pop(cols.index("task")))
        df = df[cols]
        tsv_file = os.path.join(group_dir, f"Global_Quality_Index_attempt_{attempt}.tsv")
        df.to_csv(tsv_file, sep="\t", index=False)

    # 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}")
