"""Validation module for assessing normalization quality.

Uses the dual-control design:
- Inter-experiment reference: calibration anchor
- Intra-experiment QC: validation control
"""

import base64
import logging
from dataclasses import dataclass, field
from datetime import datetime
from io import BytesIO
from pathlib import Path

import numpy as np
import pandas as pd
from sklearn.decomposition import PCA

logger = logging.getLogger(__name__)

# Check for optional visualization dependencies
try:
    import matplotlib

    matplotlib.use("Agg")  # Non-interactive backend
    import matplotlib.pyplot as plt

    HAS_MATPLOTLIB = True
except ImportError:
    HAS_MATPLOTLIB = False
    plt = None


@dataclass
class ValidationMetrics:
    """Metrics for assessing normalization quality."""

    # CV metrics
    reference_cv_before: float
    reference_cv_after: float
    qc_cv_before: float
    qc_cv_after: float

    # CV improvement
    reference_cv_improvement: float  # (before - after) / before
    qc_cv_improvement: float
    relative_variance_reduction: float  # qc improvement / reference improvement

    # PCA metrics
    pca_qc_reference_distance_before: float
    pca_qc_reference_distance_after: float
    pca_distance_ratio: float  # after / before (should be ~1, not << 1)

    # Warnings
    warnings: list[str] = field(default_factory=list)

    @property
    def passed(self) -> bool:
        """Check if validation passed basic criteria."""
        return (
            self.qc_cv_improvement > 0  # QC CV improved
            and self.pca_distance_ratio > 0.5  # Didn't collapse QC into reference
            and self.relative_variance_reduction < 2.0  # Didn't overfit to reference
        )


def calculate_cv(
    data: pd.DataFrame,
    sample_mask: pd.Series,
    abundance_col: str = "abundance",
    precursor_col: str = "precursor_id",
    replicate_col: str = "replicate_name",
) -> float:
    """Calculate median coefficient of variation across peptides.

    Args:
        data: DataFrame with peptide data
        sample_mask: Boolean mask for samples to include
        abundance_col: Column with abundance values
        precursor_col: Column with precursor identifiers
        replicate_col: Column with replicate names

    Returns:
        Median CV across peptides

    """
    subset = data.loc[sample_mask]

    # Pivot to wide format
    matrix = subset.pivot_table(
        index=precursor_col,
        columns=replicate_col,
        values=abundance_col,
    )

    # Calculate CV per peptide (on linear scale)
    linear = np.power(2, matrix)
    cv_per_peptide = linear.std(axis=1) / linear.mean(axis=1)

    return cv_per_peptide.median()


def calculate_pca_distance(
    data: pd.DataFrame,
    qc_mask: pd.Series,
    reference_mask: pd.Series,
    abundance_col: str = "abundance",
    precursor_col: str = "precursor_id",
    replicate_col: str = "replicate_name",
    n_components: int = 2,
) -> float:
    """Calculate distance between QC and reference centroids in PCA space.

    Args:
        data: DataFrame with peptide data
        qc_mask: Boolean mask for QC samples
        reference_mask: Boolean mask for reference samples
        abundance_col: Column with abundance values
        precursor_col: Column with precursor identifiers
        replicate_col: Column with replicate names
        n_components: Number of PCA components

    Returns:
        Euclidean distance between QC and reference centroids in PC space

    """
    # Get QC and reference samples
    control_mask = qc_mask | reference_mask
    subset = data.loc[control_mask]

    # Pivot to wide format (samples as rows, peptides as columns)
    matrix = subset.pivot_table(
        index=replicate_col,
        columns=precursor_col,
        values=abundance_col,
    )

    # Handle missing values - drop peptides with too many missing
    matrix = matrix.dropna(axis=1, thresh=len(matrix) * 0.5)
    matrix = matrix.fillna(matrix.median())

    if matrix.shape[1] < n_components:
        logger.warning(f"Too few peptides for PCA: {matrix.shape[1]}")
        return np.nan

    # Fit PCA
    pca = PCA(n_components=n_components)
    scores = pca.fit_transform(matrix.values)
    scores_df = pd.DataFrame(
        scores, index=matrix.index, columns=[f"PC{i + 1}" for i in range(n_components)]
    )

    # Get sample names for each type
    qc_samples = data.loc[qc_mask, replicate_col].unique()
    ref_samples = data.loc[reference_mask, replicate_col].unique()

    # Calculate centroids
    qc_centroid = scores_df.loc[scores_df.index.isin(qc_samples)].mean()
    ref_centroid = scores_df.loc[scores_df.index.isin(ref_samples)].mean()

    # Euclidean distance
    distance = np.sqrt(((qc_centroid - ref_centroid) ** 2).sum())

    return distance


def validate_correction(
    data_before: pd.DataFrame,
    data_after: pd.DataFrame,
    sample_type_col: str = "sample_type",
    abundance_col_before: str = "abundance",
    abundance_col_after: str = "abundance_normalized",
    precursor_col: str = "precursor_id",
    replicate_col: str = "replicate_name",
) -> ValidationMetrics:
    """Assess whether correction improved data quality without overcorrection.

    Uses the dual-control design:
    - Reference samples: should show CV improvement (technical variation removed)
    - QC samples: should show similar CV improvement AND remain distinct from reference

    Args:
        data_before: DataFrame with original data
        data_after: DataFrame with normalized data
        sample_type_col: Column indicating sample type
        abundance_col_before: Abundance column in before data
        abundance_col_after: Abundance column in after data
        precursor_col: Column with precursor identifiers
        replicate_col: Column with replicate names

    Returns:
        ValidationMetrics with assessment results

    """
    logger.info("Validating normalization quality")

    warnings = []

    # Create masks for sample types
    qc_mask_before = data_before[sample_type_col] == "qc"
    ref_mask_before = data_before[sample_type_col] == "reference"
    qc_mask_after = data_after[sample_type_col] == "qc"
    ref_mask_after = data_after[sample_type_col] == "reference"

    # Calculate CVs before
    ref_cv_before = calculate_cv(
        data_before, ref_mask_before, abundance_col_before, precursor_col, replicate_col
    )
    qc_cv_before = calculate_cv(
        data_before, qc_mask_before, abundance_col_before, precursor_col, replicate_col
    )

    # Calculate CVs after
    ref_cv_after = calculate_cv(
        data_after, ref_mask_after, abundance_col_after, precursor_col, replicate_col
    )
    qc_cv_after = calculate_cv(
        data_after, qc_mask_after, abundance_col_after, precursor_col, replicate_col
    )

    # CV improvements
    ref_cv_improvement = (ref_cv_before - ref_cv_after) / ref_cv_before if ref_cv_before > 0 else 0
    qc_cv_improvement = (qc_cv_before - qc_cv_after) / qc_cv_before if qc_cv_before > 0 else 0

    # Relative variance reduction
    rvr = qc_cv_improvement / ref_cv_improvement if ref_cv_improvement > 0 else np.inf

    # Check for warnings
    if qc_cv_improvement < 0:
        warnings.append("QC CV increased after normalization")
    if ref_cv_improvement < 0:
        warnings.append("Reference CV increased after normalization")
    if rvr > 2.0:
        warnings.append(
            f"QC improved much more than reference (RVR={rvr:.2f}) - possible overfitting"
        )
    if rvr < 0.5:
        warnings.append(
            f"QC improved much less than reference (RVR={rvr:.2f}) - normalization may not generalize"
        )

    # Calculate PCA distances
    pca_dist_before = calculate_pca_distance(
        data_before,
        qc_mask_before,
        ref_mask_before,
        abundance_col_before,
        precursor_col,
        replicate_col,
    )
    pca_dist_after = calculate_pca_distance(
        data_after, qc_mask_after, ref_mask_after, abundance_col_after, precursor_col, replicate_col
    )

    pca_ratio = pca_dist_after / pca_dist_before if pca_dist_before > 0 else np.nan

    if pca_ratio < 0.5:
        warnings.append(
            f"QC-reference PCA distance decreased by {(1 - pca_ratio) * 100:.1f}% - "
            "samples may be collapsing together"
        )

    metrics = ValidationMetrics(
        reference_cv_before=ref_cv_before,
        reference_cv_after=ref_cv_after,
        qc_cv_before=qc_cv_before,
        qc_cv_after=qc_cv_after,
        reference_cv_improvement=ref_cv_improvement,
        qc_cv_improvement=qc_cv_improvement,
        relative_variance_reduction=rvr,
        pca_qc_reference_distance_before=pca_dist_before,
        pca_qc_reference_distance_after=pca_dist_after,
        pca_distance_ratio=pca_ratio,
        warnings=warnings,
    )

    # Log summary
    logger.info(
        f"Reference CV: {ref_cv_before:.3f} -> {ref_cv_after:.3f} "
        f"({ref_cv_improvement * 100:.1f}% improvement)"
    )
    logger.info(
        f"QC CV: {qc_cv_before:.3f} -> {qc_cv_after:.3f} "
        f"({qc_cv_improvement * 100:.1f}% improvement)"
    )
    logger.info(f"PCA distance ratio: {pca_ratio:.2f}")

    if warnings:
        for w in warnings:
            logger.warning(w)

    if metrics.passed:
        logger.info("Validation PASSED")
    else:
        logger.warning("Validation FAILED - review warnings")

    return metrics


def generate_qc_report(
    metrics: ValidationMetrics,
    normalization_log: list[str],
    output_path: str,
    data_before: pd.DataFrame | None = None,
    data_after: pd.DataFrame | None = None,
    data_batch_corrected: pd.DataFrame | None = None,
    reference_stats: pd.DataFrame | None = None,
    sample_type_col: str = "sample_type",
    precursor_col: str = "precursor_id",
    sample_col: str = "replicate_name",
    abundance_col: str = "abundance",
    rt_col: str = "retention_time",
    save_plots: bool = True,
    embed_plots: bool = True,
) -> dict[str, str]:
    """Generate HTML QC report with embedded or linked PNG plots.

    Args:
        metrics: ValidationMetrics from validate_correction
        normalization_log: List of processing steps applied
        output_path: Path to save HTML report
        data_before: Optional original data for plots
        data_after: Optional normalized data for plots
        data_batch_corrected: Optional batch-corrected data for plots
        reference_stats: Optional reference statistics for RT plots
        sample_type_col: Column name with sample type
        precursor_col: Column name with precursor identifiers
        sample_col: Column name with sample identifiers
        abundance_col: Column name with abundance values
        rt_col: Column name with retention times
        save_plots: Whether to save individual PNG files
        embed_plots: Whether to embed plots in HTML (base64)

    Returns:
        Dict mapping plot names to file paths (if save_plots=True)

    """
    output_path = Path(output_path)
    plots_dir = output_path.parent / "qc_plots"
    plot_paths = {}

    # Create plots directory if saving
    if save_plots:
        plots_dir.mkdir(parents=True, exist_ok=True)

    # Generate plots if we have data and matplotlib
    plot_html_sections = []

    if HAS_MATPLOTLIB and data_before is not None:
        from . import visualization as viz

        # Get sample types mapping
        sample_types = None
        if sample_type_col in data_before.columns:
            sample_types = (
                data_before.drop_duplicates(sample_col)
                .set_index(sample_col)[sample_type_col]
                .to_dict()
            )

        # 1. Intensity Distribution Comparison
        if data_after is not None:
            try:
                fig = viz.plot_normalization_comparison(
                    data_before,
                    data_after,
                    sample_col=sample_col,
                    abundance_col_before=abundance_col,
                    abundance_col_after=abundance_col,
                    title="Intensity Distribution: Before vs After Normalization",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "intensity_distribution", plots_dir, save_plots, embed_plots
                    )
                    plot_html_sections.append(("Intensity Distribution", plot_html))
                    if plot_path:
                        plot_paths["intensity_distribution"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate intensity distribution plot: {e}")

        # 2. PCA Comparison
        try:
            fig = viz.plot_comparative_pca(
                data_before,
                data_after if data_after is not None else data_before,
                data_batch_corrected,
                sample_col=sample_col,
                precursor_col=precursor_col,
                abundance_col_original=abundance_col,
                abundance_col_normalized=abundance_col,
                abundance_col_corrected=abundance_col,
                sample_groups=sample_types,
                show_plot=False,
            )
            if fig is not None:
                plot_html, plot_path = _save_and_embed_plot(
                    fig, "pca_comparison", plots_dir, save_plots, embed_plots
                )
                plot_html_sections.append(("PCA Analysis", plot_html))
                if plot_path:
                    plot_paths["pca_comparison"] = str(plot_path)
                plt.close(fig)
        except Exception as e:
            logger.warning(f"Failed to generate PCA plot: {e}")

        # 3. Control Sample Correlation
        try:
            fig, _ = viz.plot_control_correlation_heatmap(
                data_after if data_after is not None else data_before,
                sample_col=sample_col,
                precursor_col=precursor_col,
                abundance_col=abundance_col,
                sample_type_col=sample_type_col,
                control_types=["reference", "qc"],
                title="Control Sample Correlation (After Normalization)",
                show_plot=False,
            )
            if fig is not None:
                plot_html, plot_path = _save_and_embed_plot(
                    fig, "control_correlation", plots_dir, save_plots, embed_plots
                )
                plot_html_sections.append(("Control Sample Correlation", plot_html))
                if plot_path:
                    plot_paths["control_correlation"] = str(plot_path)
                plt.close(fig)
        except Exception as e:
            logger.warning(f"Failed to generate correlation heatmap: {e}")

        # 4. CV Distribution Comparison
        if data_after is not None:
            try:
                fig = viz.plot_comparative_cv(
                    data_before,
                    data_after,
                    sample_col=sample_col,
                    precursor_col=precursor_col,
                    abundance_col_before=abundance_col,
                    abundance_col_after=abundance_col,
                    sample_type_col=sample_type_col,
                    control_type="reference",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "cv_distribution_reference", plots_dir, save_plots, embed_plots
                    )
                    plot_html_sections.append(("CV Distribution (Reference)", plot_html))
                    if plot_path:
                        plot_paths["cv_distribution_reference"] = str(plot_path)
                    plt.close(fig)

                # QC CV
                fig = viz.plot_comparative_cv(
                    data_before,
                    data_after,
                    sample_col=sample_col,
                    precursor_col=precursor_col,
                    abundance_col_before=abundance_col,
                    abundance_col_after=abundance_col,
                    sample_type_col=sample_type_col,
                    control_type="qc",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "cv_distribution_qc", plots_dir, save_plots, embed_plots
                    )
                    plot_html_sections.append(("CV Distribution (QC)", plot_html))
                    if plot_path:
                        plot_paths["cv_distribution_qc"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate CV distribution plot: {e}")

        # 5. RT Correction Plots (if RT data and reference stats available)
        if reference_stats is not None and rt_col in data_before.columns and data_after is not None:
            try:
                fig = viz.plot_rt_correction_comparison(
                    data_before,
                    data_after,
                    reference_stats,
                    sample_col=sample_col,
                    precursor_col=precursor_col,
                    abundance_col=abundance_col,
                    rt_col=rt_col,
                    sample_type_col=sample_type_col,
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "rt_correction", plots_dir, save_plots, embed_plots
                    )
                    plot_html_sections.append(("RT-Dependent Normalization QC", plot_html))
                    if plot_path:
                        plot_paths["rt_correction"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate RT correction plot: {e}")

    # Build plots HTML
    plots_html = ""
    for title, plot_content in plot_html_sections:
        plots_html += f"""
        <div class="plot-section">
            <h3>{title}</h3>
            {plot_content}
        </div>
        """

    # Generate the HTML report
    html = f"""<!DOCTYPE html>
<html>
<head>
    <title>PRISM QC Report</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
               margin: 0; padding: 20px; background: #f5f5f5; }}
        .container {{ max-width: 1200px; margin: 0 auto; background: white;
                     padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        h1 {{ color: #333; border-bottom: 2px solid #0066cc; padding-bottom: 10px; }}
        h2 {{ color: #444; margin-top: 30px; border-bottom: 1px solid #ddd; padding-bottom: 8px; }}
        h3 {{ color: #555; }}
        .metric {{ margin: 10px 0; padding: 8px; background: #f9f9f9; border-radius: 4px; }}
        .metric-name {{ font-weight: 600; color: #333; }}
        .metric-value {{ color: #0066cc; font-family: monospace; font-size: 1.1em; }}
        .warning {{ color: #856404; background: #fff3cd; border: 1px solid #ffc107;
                   padding: 12px; margin: 8px 0; border-radius: 4px; }}
        .passed {{ color: #155724; background: #d4edda; border: 1px solid #28a745;
                  padding: 15px; border-radius: 4px; font-size: 1.1em; }}
        .failed {{ color: #721c24; background: #f8d7da; border: 1px solid #dc3545;
                  padding: 15px; border-radius: 4px; font-size: 1.1em; }}
        table {{ border-collapse: collapse; margin: 20px 0; width: 100%; }}
        th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
        th {{ background: #f0f0f0; font-weight: 600; }}
        tr:nth-child(even) {{ background: #f9f9f9; }}
        .plot-section {{ margin: 20px 0; text-align: center; }}
        .plot-section img {{ max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; }}
        .timestamp {{ color: #888; font-size: 0.9em; margin-top: 30px; padding-top: 20px;
                     border-top: 1px solid #ddd; }}
        ol {{ line-height: 1.8; }}
        .improvement-positive {{ color: #28a745; }}
        .improvement-negative {{ color: #dc3545; }}
    </style>
</head>
<body>
    <div class="container">
        <h1>🔬 PRISM QC Report</h1>

        <h2>Validation Status</h2>
        <div class="{"passed" if metrics.passed else "failed"}">
            <strong>{"✓ PASSED" if metrics.passed else "✗ FAILED"}</strong> —
            {"All validation criteria met" if metrics.passed else "Review warnings below"}
        </div>

        <h2>CV Metrics</h2>
        <table>
            <tr>
                <th>Sample Type</th>
                <th>CV Before</th>
                <th>CV After</th>
                <th>Improvement</th>
            </tr>
            <tr>
                <td><strong>Reference</strong> (calibration)</td>
                <td>{metrics.reference_cv_before:.3f}</td>
                <td>{metrics.reference_cv_after:.3f}</td>
                <td class="{"improvement-positive" if metrics.reference_cv_improvement > 0 else "improvement-negative"}">
                    {metrics.reference_cv_improvement * 100:+.1f}%
                </td>
            </tr>
            <tr>
                <td><strong>QC</strong> (validation)</td>
                <td>{metrics.qc_cv_before:.3f}</td>
                <td>{metrics.qc_cv_after:.3f}</td>
                <td class="{"improvement-positive" if metrics.qc_cv_improvement > 0 else "improvement-negative"}">
                    {metrics.qc_cv_improvement * 100:+.1f}%
                </td>
            </tr>
        </table>

        <div class="metric">
            <span class="metric-name">Relative Variance Reduction (RVR):</span>
            <span class="metric-value">{metrics.relative_variance_reduction:.2f}</span>
            <br><small style="color: #666;">Target: ~1.0 | &gt;&gt;1 suggests overfitting | &lt;&lt;1 suggests poor generalization</small>
        </div>

        <h2>PCA Metrics</h2>
        <table>
            <tr>
                <th>Metric</th>
                <th>Value</th>
                <th>Interpretation</th>
            </tr>
            <tr>
                <td>QC-Reference Distance (Before)</td>
                <td>{metrics.pca_qc_reference_distance_before:.2f}</td>
                <td>Baseline separation</td>
            </tr>
            <tr>
                <td>QC-Reference Distance (After)</td>
                <td>{metrics.pca_qc_reference_distance_after:.2f}</td>
                <td>Post-normalization separation</td>
            </tr>
            <tr>
                <td>Distance Ratio</td>
                <td><strong>{metrics.pca_distance_ratio:.2f}</strong></td>
                <td>{"Good - samples remain distinct" if metrics.pca_distance_ratio > 0.5 else "WARNING - Samples may be collapsing"}</td>
            </tr>
        </table>

        <h2>Warnings</h2>
        {"".join(f'<div class="warning">⚠️ {w}</div>' for w in metrics.warnings) if metrics.warnings else '<p style="color: #28a745;">✓ No warnings</p>'}

        <h2>QC Plots</h2>
        {plots_html if plots_html else '<p style="color: #888;">No plots generated (data not provided or matplotlib unavailable)</p>'}

        <h2>Processing Steps</h2>
        <ol>
            {"".join(f"<li>{step}</li>" for step in normalization_log) if normalization_log else "<li>No processing steps recorded</li>"}
        </ol>

        <div class="timestamp">
            Report generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
        </div>
    </div>
</body>
</html>"""

    # Write the HTML file
    with open(output_path, "w") as f:
        f.write(html)

    logger.info(f"QC report saved to {output_path}")
    if save_plots and plot_paths:
        logger.info(f"QC plots saved to {plots_dir}")

    return plot_paths


def _save_and_embed_plot(
    fig,
    name: str,
    plots_dir: Path,
    save_plots: bool,
    embed_plots: bool,
) -> tuple[str, Path | None]:
    """Save a matplotlib figure and/or generate HTML for embedding.

    Args:
        fig: matplotlib Figure object
        name: Base name for the plot file
        plots_dir: Directory to save plots
        save_plots: Whether to save PNG file
        embed_plots: Whether to embed as base64

    Returns:
        Tuple of (HTML string for embedding, path if saved)

    """
    plot_path = None
    html = ""

    if save_plots:
        plot_path = plots_dir / f"{name}.png"
        fig.savefig(plot_path, dpi=150, bbox_inches="tight", facecolor="white")

    if embed_plots:
        # Convert to base64 for embedding
        buffer = BytesIO()
        fig.savefig(buffer, format="png", dpi=150, bbox_inches="tight", facecolor="white")
        buffer.seek(0)
        img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
        html = f'<img src="data:image/png;base64,{img_base64}" alt="{name}" />'
    elif plot_path:
        # Link to saved file
        html = f'<img src="qc_plots/{name}.png" alt="{name}" />'

    return html, plot_path


def generate_comprehensive_qc_report(
    peptide_raw: pd.DataFrame,
    peptide_corrected: pd.DataFrame,
    protein_raw: pd.DataFrame,
    protein_corrected: pd.DataFrame,
    sample_cols: list[str],
    sample_types: dict[str, str],
    output_path: str | Path,
    method_log: list[str],
    config: dict | None = None,
    save_plots: bool = True,
    embed_plots: bool = True,
    peptide_rawsum: pd.DataFrame | None = None,
    peptide_medianpolish: pd.DataFrame | None = None,
    rt_lowess_result: "RTLowessResult | None" = None,
    sample_batches: dict[str, str] | None = None,
    pipeline_metadata: dict | None = None,
) -> dict[str, str]:
    """Generate comprehensive QC report with multi-stage plots for peptides and proteins.

    Creates an HTML report with diagnostic plots comparing processing stages.
    If peptide_rawsum and peptide_medianpolish are provided, generates 3-stage
    comparison plots (rawsum -> medianpolish -> normalized).
    If rt_lowess_result is provided, generates RT-lowess specific QC plots.

    Args:
        peptide_raw: Wide-format DataFrame with raw peptide abundances (legacy, used if
                     peptide_medianpolish not provided)
        peptide_corrected: Wide-format DataFrame with corrected peptide abundances
        protein_raw: Wide-format DataFrame with raw protein abundances
        protein_corrected: Wide-format DataFrame with corrected protein abundances
        sample_cols: List of column names containing sample abundances
        sample_types: Dict mapping sample name to type ('experimental', 'qc', 'reference')
        output_path: Path to save HTML report
        method_log: List of processing steps applied
        config: Optional config dict for QC report settings
        save_plots: Whether to save individual PNG files
        embed_plots: Whether to embed plots in HTML (base64)
        peptide_rawsum: Wide-format DataFrame with sum-aggregated peptides (stage 1)
        peptide_medianpolish: Wide-format DataFrame after median polish (stage 2)
        rt_lowess_result: Optional RTLowessResult from RT-lowess normalization
        sample_batches: Optional dict mapping sample name to batch for batch coloring
        pipeline_metadata: Optional dict with source files, processing params, etc.

    Returns:
        Dict mapping plot names to file paths (if save_plots=True)

    """
    output_path = Path(output_path)
    plots_dir = output_path.parent / "qc_plots"
    plot_paths = {}
    peptide_plot_sections = []
    protein_plot_sections = []

    # Determine if we have 3-stage data
    has_three_stage = peptide_rawsum is not None and peptide_medianpolish is not None

    # Create plots directory if saving
    if save_plots:
        plots_dir.mkdir(parents=True, exist_ok=True)

    # Get config settings
    if config is None:
        config = {}
    qc_config = config.get("qc_report", {})
    plot_settings = qc_config.get("plots", {})

    # Check if matplotlib is available
    if not HAS_MATPLOTLIB:
        logger.warning("matplotlib not available - skipping QC plots")
    else:
        from . import visualization as viz

        # =====================================================================
        # PEPTIDE-LEVEL PLOTS
        # =====================================================================
        logger.info("Generating peptide-level QC plots...")

        # 1. Peptide Intensity Distribution Comparison
        if plot_settings.get("intensity_distribution", True):
            try:
                # Use median polish as "raw" for intensity comparison
                raw_for_intensity = peptide_medianpolish if has_three_stage else peptide_raw
                fig = viz.plot_normalization_comparison_wide(
                    raw_for_intensity,
                    peptide_corrected,
                    sample_cols,
                    title="Peptide Intensity Distribution",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "peptide_intensity_distribution", plots_dir, save_plots, embed_plots
                    )
                    peptide_plot_sections.append(("Peptide Intensity Distribution", plot_html))
                    if plot_path:
                        plot_paths["peptide_intensity_distribution"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate peptide intensity plot: {e}")

        # 1b. Peptide Box Plot Comparison (3-stage if available, 2-stage otherwise)
        if plot_settings.get("boxplot_comparison", True):
            try:
                if has_three_stage:
                    fig = viz.plot_boxplot_three_stage(
                        peptide_rawsum,
                        peptide_medianpolish,
                        peptide_corrected,
                        sample_cols,
                        sample_types=sample_types,
                        title="Peptide Intensity: Processing Stages",
                        show_plot=False,
                    )
                    plot_title = "Peptide Box Plot (3 Stages)"
                else:
                    fig = viz.plot_boxplot_two_stage(
                        peptide_raw,
                        peptide_corrected,
                        sample_cols,
                        sample_types=sample_types,
                        before_label="Raw (Sum Rollup)",
                        after_label="Normalized + Corrected",
                        title="Peptide Intensity: Before vs After",
                        show_plot=False,
                    )
                    plot_title = "Peptide Box Plot Comparison"
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "peptide_boxplot_comparison", plots_dir, save_plots, embed_plots
                    )
                    peptide_plot_sections.append((plot_title, plot_html))
                    if plot_path:
                        plot_paths["peptide_boxplot_comparison"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate peptide boxplot: {e}")

        # 2. Peptide PCA Comparison (3-stage if available)
        if plot_settings.get("pca_comparison", True):
            try:
                if has_three_stage:
                    fig = viz.plot_pca_three_stage(
                        peptide_rawsum,
                        peptide_medianpolish,
                        peptide_corrected,
                        sample_cols,
                        sample_types=sample_types,
                        title="Peptide PCA: Processing Stages",
                        show_plot=False,
                    )
                else:
                    fig = viz.plot_comparative_pca_wide(
                        peptide_raw,
                        peptide_corrected,
                        sample_cols,
                        sample_types=sample_types,
                        title="Peptide PCA: Before vs After Normalization",
                        show_plot=False,
                    )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "peptide_pca_comparison", plots_dir, save_plots, embed_plots
                    )
                    peptide_plot_sections.append(("Peptide PCA Analysis", plot_html))
                    if plot_path:
                        plot_paths["peptide_pca_comparison"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate peptide PCA plot: {e}")

        # 3. Peptide CV Distribution - Reference Samples (3-stage if available)
        if plot_settings.get("cv_distribution", True):
            # Check if we have reference samples
            ref_cols = [c for c in sample_cols if sample_types.get(c) == "reference"]
            if len(ref_cols) >= 2:
                try:
                    if has_three_stage:
                        fig = viz.plot_cv_three_stage(
                            peptide_rawsum,
                            peptide_medianpolish,
                            peptide_corrected,
                            sample_cols,
                            sample_types=sample_types,
                            control_type="reference",
                            title="",
                            show_plot=False,
                        )
                    else:
                        fig = viz.plot_cv_comparison_wide(
                            peptide_raw,
                            peptide_corrected,
                            sample_cols,
                            sample_types=sample_types,
                            control_type="reference",
                            title="",
                            show_plot=False,
                        )
                    if fig is not None:
                        plot_html, plot_path = _save_and_embed_plot(
                            fig, "peptide_cv_reference", plots_dir, save_plots, embed_plots
                        )
                        peptide_plot_sections.append(("Peptide CV (Reference Samples)", plot_html))
                        if plot_path:
                            plot_paths["peptide_cv_reference"] = str(plot_path)
                        plt.close(fig)
                except Exception as e:
                    logger.warning(f"Failed to generate peptide CV (ref) plot: {e}")

            # 4. Peptide CV Distribution - QC Samples (3-stage if available)
            qc_cols = [c for c in sample_cols if sample_types.get(c) == "qc"]
            if len(qc_cols) >= 2:
                try:
                    if has_three_stage:
                        fig = viz.plot_cv_three_stage(
                            peptide_rawsum,
                            peptide_medianpolish,
                            peptide_corrected,
                            sample_cols,
                            sample_types=sample_types,
                            control_type="qc",
                            title="",
                            show_plot=False,
                        )
                    else:
                        fig = viz.plot_cv_comparison_wide(
                            peptide_raw,
                            peptide_corrected,
                            sample_cols,
                            sample_types=sample_types,
                            control_type="qc",
                            title="",
                            show_plot=False,
                        )
                    if fig is not None:
                        plot_html, plot_path = _save_and_embed_plot(
                            fig, "peptide_cv_qc", plots_dir, save_plots, embed_plots
                        )
                        peptide_plot_sections.append(("Peptide CV (QC Samples)", plot_html))
                        if plot_path:
                            plot_paths["peptide_cv_qc"] = str(plot_path)
                        plt.close(fig)
                except Exception as e:
                    logger.warning(f"Failed to generate peptide CV (qc) plot: {e}")

        # 5. Peptide Control Correlation
        if plot_settings.get("control_correlation", True):
            try:
                fig, _ = viz.plot_control_correlation_wide(
                    peptide_corrected,
                    sample_cols,
                    sample_types=sample_types,
                    control_types=["reference", "qc"],
                    title="Peptide Control Sample Correlation (After Correction)",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "peptide_control_correlation", plots_dir, save_plots, embed_plots
                    )
                    peptide_plot_sections.append(("Peptide Control Correlation", plot_html))
                    if plot_path:
                        plot_paths["peptide_control_correlation"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate peptide correlation plot: {e}")

        # =====================================================================
        # RT-LOWESS NORMALIZATION PLOTS
        # =====================================================================
        if rt_lowess_result is not None:
            logger.info("Generating RT-lowess normalization QC plots...")

            # 5a. RT-Lowess Overlay Comparison
            if plot_settings.get("rt_lowess_overlay", True):
                try:
                    fig = viz.plot_rt_lowess_overlay_comparison(
                        rt_lowess_result.sample_curves,
                        rt_lowess_result.global_curve,
                        rt_lowess_result.rt_grid,
                        sample_batches=sample_batches,
                        title="RT-Lowess Normalization: Sample Curves",
                        show_plot=False,
                    )
                    if fig is not None:
                        plot_html, plot_path = _save_and_embed_plot(
                            fig, "rt_lowess_overlay", plots_dir, save_plots, embed_plots
                        )
                        peptide_plot_sections.append(("RT-Lowess Curves", plot_html))
                        if plot_path:
                            plot_paths["rt_lowess_overlay"] = str(plot_path)
                        plt.close(fig)
                except Exception as e:
                    logger.warning(f"Failed to generate RT-lowess overlay plot: {e}")

            # 5b. RT Bin Boxplot Comparison
            if plot_settings.get("rt_bin_boxplot", True):
                try:
                    # Use medianpolish data as "before" if available, else rawsum
                    data_before = peptide_medianpolish if has_three_stage else peptide_raw
                    fig = viz.plot_rt_bin_boxplot_comparison(
                        data_before,
                        peptide_corrected,
                        sample_cols,
                        rt_col="mean_rt",
                        sample_batches=sample_batches,
                        n_bins=8,
                        title="Abundance Distribution by RT Bin",
                        show_plot=False,
                    )
                    if fig is not None:
                        plot_html, plot_path = _save_and_embed_plot(
                            fig, "rt_bin_boxplot", plots_dir, save_plots, embed_plots
                        )
                        peptide_plot_sections.append(("RT Bin Box Plots", plot_html))
                        if plot_path:
                            plot_paths["rt_bin_boxplot"] = str(plot_path)
                        plt.close(fig)
                except Exception as e:
                    logger.warning(f"Failed to generate RT bin boxplot: {e}")

            # 5c. RT Bin CV Comparison
            if plot_settings.get("rt_bin_cv", True):
                try:
                    data_before = peptide_medianpolish if has_three_stage else peptide_raw
                    fig = viz.plot_rt_bin_cv_comparison(
                        data_before,
                        peptide_corrected,
                        sample_cols,
                        rt_col="mean_rt",
                        sample_types=sample_types,
                        n_bins=8,
                        title="RT-Local CV: Before vs After Normalization",
                        show_plot=False,
                    )
                    if fig is not None:
                        plot_html, plot_path = _save_and_embed_plot(
                            fig, "rt_bin_cv", plots_dir, save_plots, embed_plots
                        )
                        peptide_plot_sections.append(("RT Bin CV Comparison", plot_html))
                        if plot_path:
                            plot_paths["rt_bin_cv"] = str(plot_path)
                        plt.close(fig)
                except Exception as e:
                    logger.warning(f"Failed to generate RT bin CV plot: {e}")

        # =====================================================================
        # PROTEIN-LEVEL PLOTS
        # =====================================================================
        logger.info("Generating protein-level QC plots...")

        # 6. Protein Intensity Distribution Comparison
        if plot_settings.get("intensity_distribution", True):
            try:
                fig = viz.plot_normalization_comparison_wide(
                    protein_raw,
                    protein_corrected,
                    sample_cols,
                    title="Protein Intensity Distribution",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "protein_intensity_distribution", plots_dir, save_plots, embed_plots
                    )
                    protein_plot_sections.append(("Protein Intensity Distribution", plot_html))
                    if plot_path:
                        plot_paths["protein_intensity_distribution"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate protein intensity plot: {e}")

        # 6b. Protein Box Plot Comparison
        if plot_settings.get("boxplot_comparison", True):
            try:
                fig = viz.plot_boxplot_two_stage(
                    protein_raw,
                    protein_corrected,
                    sample_cols,
                    sample_types=sample_types,
                    before_label="Raw (Median Polish Rollup)",
                    after_label="Normalized + Corrected",
                    title="Protein Intensity: Before vs After",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "protein_boxplot_comparison", plots_dir, save_plots, embed_plots
                    )
                    protein_plot_sections.append(("Protein Box Plot Comparison", plot_html))
                    if plot_path:
                        plot_paths["protein_boxplot_comparison"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate protein boxplot: {e}")

        # 7. Protein PCA Comparison
        if plot_settings.get("pca_comparison", True):
            try:
                fig = viz.plot_comparative_pca_wide(
                    protein_raw,
                    protein_corrected,
                    sample_cols,
                    sample_types=sample_types,
                    title="Protein PCA: Before vs After Normalization",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "protein_pca_comparison", plots_dir, save_plots, embed_plots
                    )
                    protein_plot_sections.append(("Protein PCA Analysis", plot_html))
                    if plot_path:
                        plot_paths["protein_pca_comparison"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate protein PCA plot: {e}")

        # 8. Protein CV Distribution - Reference Samples
        if plot_settings.get("cv_distribution", True):
            ref_cols = [c for c in sample_cols if sample_types.get(c) == "reference"]
            if len(ref_cols) >= 2:
                try:
                    fig = viz.plot_cv_comparison_wide(
                        protein_raw,
                        protein_corrected,
                        sample_cols,
                        sample_types=sample_types,
                        control_type="reference",
                        title="",
                        show_plot=False,
                    )
                    if fig is not None:
                        plot_html, plot_path = _save_and_embed_plot(
                            fig, "protein_cv_reference", plots_dir, save_plots, embed_plots
                        )
                        protein_plot_sections.append(("Protein CV (Reference Samples)", plot_html))
                        if plot_path:
                            plot_paths["protein_cv_reference"] = str(plot_path)
                        plt.close(fig)
                except Exception as e:
                    logger.warning(f"Failed to generate protein CV (ref) plot: {e}")

            # 9. Protein CV Distribution - QC Samples
            qc_cols = [c for c in sample_cols if sample_types.get(c) == "qc"]
            if len(qc_cols) >= 2:
                try:
                    fig = viz.plot_cv_comparison_wide(
                        protein_raw,
                        protein_corrected,
                        sample_cols,
                        sample_types=sample_types,
                        control_type="qc",
                        title="",
                        show_plot=False,
                    )
                    if fig is not None:
                        plot_html, plot_path = _save_and_embed_plot(
                            fig, "protein_cv_qc", plots_dir, save_plots, embed_plots
                        )
                        protein_plot_sections.append(("Protein CV (QC Samples)", plot_html))
                        if plot_path:
                            plot_paths["protein_cv_qc"] = str(plot_path)
                        plt.close(fig)
                except Exception as e:
                    logger.warning(f"Failed to generate protein CV (qc) plot: {e}")

        # 10. Protein Control Correlation
        if plot_settings.get("control_correlation", True):
            try:
                fig, _ = viz.plot_control_correlation_wide(
                    protein_corrected,
                    sample_cols,
                    sample_types=sample_types,
                    control_types=["reference", "qc"],
                    title="Protein Control Sample Correlation (After Correction)",
                    show_plot=False,
                )
                if fig is not None:
                    plot_html, plot_path = _save_and_embed_plot(
                        fig, "protein_control_correlation", plots_dir, save_plots, embed_plots
                    )
                    protein_plot_sections.append(("Protein Control Correlation", plot_html))
                    if plot_path:
                        plot_paths["protein_control_correlation"] = str(plot_path)
                    plt.close(fig)
            except Exception as e:
                logger.warning(f"Failed to generate protein correlation plot: {e}")

    # =========================================================================
    # Calculate Summary Metrics (now with 3-stage support)
    # =========================================================================
    def calc_median_cv(df, cols):
        """Calculate median CV across features."""
        linear_data = 2 ** df[cols]
        cv_values = (linear_data.std(axis=1) / linear_data.mean(axis=1)) * 100
        return cv_values.median()

    metrics = {}
    ref_cols = [c for c in sample_cols if sample_types.get(c) == "reference"]
    qc_cols = [c for c in sample_cols if sample_types.get(c) == "qc"]

    # Use 3-stage data if available
    peptide_raw_for_metrics = peptide_rawsum if has_three_stage else peptide_raw
    peptide_mid_for_metrics = peptide_medianpolish if has_three_stage else None

    if len(ref_cols) >= 2:
        metrics["peptide_ref_cv_rawsum"] = calc_median_cv(peptide_raw_for_metrics, ref_cols)
        if peptide_mid_for_metrics is not None:
            metrics["peptide_ref_cv_medpol"] = calc_median_cv(peptide_mid_for_metrics, ref_cols)
        metrics["peptide_ref_cv_normalized"] = calc_median_cv(peptide_corrected, ref_cols)
        metrics["protein_ref_cv_before"] = calc_median_cv(protein_raw, ref_cols)
        metrics["protein_ref_cv_after"] = calc_median_cv(protein_corrected, ref_cols)

    if len(qc_cols) >= 2:
        metrics["peptide_qc_cv_rawsum"] = calc_median_cv(peptide_raw_for_metrics, qc_cols)
        if peptide_mid_for_metrics is not None:
            metrics["peptide_qc_cv_medpol"] = calc_median_cv(peptide_mid_for_metrics, qc_cols)
        metrics["peptide_qc_cv_normalized"] = calc_median_cv(peptide_corrected, qc_cols)
        metrics["protein_qc_cv_before"] = calc_median_cv(protein_raw, qc_cols)
        metrics["protein_qc_cv_after"] = calc_median_cv(protein_corrected, qc_cols)

    # =========================================================================
    # Build peptide plots HTML
    # =========================================================================
    peptide_plots_html = ""
    for title, plot_content in peptide_plot_sections:
        peptide_plots_html += f"""
        <div class="plot-section">
            <h3>{title}</h3>
            {plot_content}
        </div>
        """

    # =========================================================================
    # Build protein plots HTML
    # =========================================================================
    protein_plots_html = ""
    for title, plot_content in protein_plot_sections:
        protein_plots_html += f"""
        <div class="plot-section">
            <h3>{title}</h3>
            {plot_content}
        </div>
        """

    # =========================================================================
    # Build metrics HTML (3-stage for peptides)
    # =========================================================================
    metrics_html = ""
    if metrics:
        metrics_html = """
        <h2>Summary Metrics (Median CV %)</h2>
        """
        # Peptide metrics table (3-stage)
        if has_three_stage:
            metrics_html += """
        <h3>Peptide-Level CV</h3>
        <table>
            <tr>
                <th>Sample Type</th>
                <th>Raw Sum</th>
                <th>Median Polish</th>
                <th>Normalized</th>
                <th>Total Improvement</th>
            </tr>
            """
            for sample_type, type_key in [("Reference", "ref"), ("QC", "qc")]:
                rawsum_key = f"peptide_{type_key}_cv_rawsum"
                medpol_key = f"peptide_{type_key}_cv_medpol"
                norm_key = f"peptide_{type_key}_cv_normalized"
                if rawsum_key in metrics and norm_key in metrics:
                    cv_rawsum = metrics[rawsum_key]
                    cv_medpol = metrics.get(medpol_key, float("nan"))
                    cv_norm = metrics[norm_key]
                    improvement = (cv_rawsum - cv_norm) / cv_rawsum * 100 if cv_rawsum > 0 else 0
                    improvement_class = (
                        "improvement-positive" if improvement > 0 else "improvement-negative"
                    )
                    metrics_html += f"""
            <tr>
                <td>{sample_type}</td>
                <td>{cv_rawsum:.1f}%</td>
                <td>{cv_medpol:.1f}%</td>
                <td>{cv_norm:.1f}%</td>
                <td class="{improvement_class}">{improvement:+.1f}%</td>
            </tr>
                    """
            metrics_html += "</table>"
        else:
            # Legacy 2-stage peptide table
            metrics_html += """
        <h3>Peptide-Level CV</h3>
        <table>
            <tr>
                <th>Sample Type</th>
                <th>Before</th>
                <th>After</th>
                <th>Improvement</th>
            </tr>
            """
            for sample_type, type_key in [("Reference", "ref"), ("QC", "qc")]:
                before_key = f"peptide_{type_key}_cv_rawsum"
                after_key = f"peptide_{type_key}_cv_normalized"
                if before_key in metrics and after_key in metrics:
                    cv_before = metrics[before_key]
                    cv_after = metrics[after_key]
                    improvement = (cv_before - cv_after) / cv_before * 100 if cv_before > 0 else 0
                    improvement_class = (
                        "improvement-positive" if improvement > 0 else "improvement-negative"
                    )
                    metrics_html += f"""
            <tr>
                <td>{sample_type}</td>
                <td>{cv_before:.1f}%</td>
                <td>{cv_after:.1f}%</td>
                <td class="{improvement_class}">{improvement:+.1f}%</td>
            </tr>
                    """
            metrics_html += "</table>"

        # Protein metrics table (always 2-stage)
        metrics_html += """
        <h3>Protein-Level CV</h3>
        <table>
            <tr>
                <th>Sample Type</th>
                <th>Before</th>
                <th>After</th>
                <th>Improvement</th>
            </tr>
        """
        for sample_type, type_key in [("Reference", "ref"), ("QC", "qc")]:
            before_key = f"protein_{type_key}_cv_before"
            after_key = f"protein_{type_key}_cv_after"
            if before_key in metrics and after_key in metrics:
                cv_before = metrics[before_key]
                cv_after = metrics[after_key]
                improvement = (cv_before - cv_after) / cv_before * 100 if cv_before > 0 else 0
                improvement_class = (
                    "improvement-positive" if improvement > 0 else "improvement-negative"
                )
                metrics_html += f"""
            <tr>
                <td>{sample_type}</td>
                <td>{cv_before:.1f}%</td>
                <td>{cv_after:.1f}%</td>
                <td class="{improvement_class}">{improvement:+.1f}%</td>
            </tr>
                """
        metrics_html += "</table>"

    # =========================================================================
    # Build pipeline metadata HTML
    # =========================================================================
    import socket

    metadata_html = ""
    if pipeline_metadata:
        # Extract info from metadata
        source_files = pipeline_metadata.get("source_files", [])
        processing_date = pipeline_metadata.get("processing_date", "N/A")
        pipeline_version = pipeline_metadata.get("pipeline_version", "N/A")
        processing_params = pipeline_metadata.get("processing_parameters", {})
        stats = pipeline_metadata.get("statistics", {})

        # Get computer name
        try:
            computer_name = socket.gethostname()
        except Exception:
            computer_name = "Unknown"

        # Format source files
        source_files_html = ""
        if source_files:
            source_files_html = "<ul style='margin: 5px 0; padding-left: 20px;'>"
            for sf in source_files[:10]:  # Limit to 10 files
                source_files_html += f"<li><code>{sf}</code></li>"
            if len(source_files) > 10:
                source_files_html += f"<li>... and {len(source_files) - 10} more</li>"
            source_files_html += "</ul>"
        else:
            source_files_html = "<em>Not recorded</em>"

        # Format processing parameters
        params_html = ""
        for section, params in processing_params.items():
            if isinstance(params, dict):
                param_items = ", ".join(f"{k}={v}" for k, v in params.items())
                params_html += f"<li><strong>{section}:</strong> {param_items}</li>"
            else:
                params_html += f"<li><strong>{section}:</strong> {params}</li>"

        metadata_html = f"""
        <div class="summary-box" style="background: #fff8e1; border-color: #ffe082;">
            <h3 style="margin-top: 0; color: #f57c00;">Analysis Information</h3>
            <table style="margin: 10px 0; width: 100%; border: none;">
                <tr style="border: none;">
                    <td style="border: none; width: 150px;"><strong>Pipeline:</strong></td>
                    <td style="border: none;">PRISM v{pipeline_version}</td>
                </tr>
                <tr style="border: none;">
                    <td style="border: none;"><strong>Processing Date:</strong></td>
                    <td style="border: none;">{processing_date}</td>
                </tr>
                <tr style="border: none;">
                    <td style="border: none;"><strong>Computer:</strong></td>
                    <td style="border: none;">{computer_name}</td>
                </tr>
                <tr style="border: none;">
                    <td style="border: none; vertical-align: top;"><strong>Source Files:</strong></td>
                    <td style="border: none;">{source_files_html}</td>
                </tr>
            </table>
            {"<h4 style='margin-bottom: 5px;'>Processing Parameters:</h4><ul style='margin: 5px 0;'>" + params_html + "</ul>" if params_html else ""}
        </div>
        """

    # =========================================================================
    # Generate the HTML report
    # =========================================================================
    html = f"""<!DOCTYPE html>
<html>
<head>
    <title>PRISM QC Report</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
               margin: 0; padding: 20px; background: #f5f5f5; }}
        .container {{ max-width: 1400px; margin: 0 auto; background: white;
                     padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        h1 {{ color: #333; border-bottom: 2px solid #0066cc; padding-bottom: 10px; }}
        h2 {{ color: #444; margin-top: 30px; border-bottom: 1px solid #ddd; padding-bottom: 8px; }}
        h3 {{ color: #555; }}
        table {{ border-collapse: collapse; margin: 20px 0; width: 100%; }}
        th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
        th {{ background: #f0f0f0; font-weight: 600; }}
        tr:nth-child(even) {{ background: #f9f9f9; }}
        .plot-section {{ margin: 30px 0; text-align: center; }}
        .plot-section img {{ max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; }}
        .timestamp {{ color: #888; font-size: 0.9em; margin-top: 30px; padding-top: 20px;
                     border-top: 1px solid #ddd; }}
        ol {{ line-height: 1.8; }}
        .improvement-positive {{ color: #28a745; font-weight: bold; }}
        .improvement-negative {{ color: #dc3545; font-weight: bold; }}
        .section-header {{ background: linear-gradient(to right, #0066cc, #0099cc);
                          color: white; padding: 10px 15px; margin: 30px 0 20px 0;
                          border-radius: 4px; }}
        .summary-box {{ background: #e8f4fc; border: 1px solid #b8daff;
                       padding: 15px; border-radius: 4px; margin: 20px 0; }}
    </style>
</head>
<body>
    <div class="container">
        <h1>PRISM QC Report</h1>

        {metadata_html}

        <div class="summary-box">
            <h3 style="margin-top: 0; color: #0066cc;">Dataset Summary</h3>
            <table style="margin: 10px 0; width: auto;">
                <tr>
                    <td><strong>Total Samples:</strong></td>
                    <td>{len(sample_cols)}</td>
                    <td style="padding-left: 30px;"><strong>Experimental:</strong></td>
                    <td>{len([c for c in sample_cols if sample_types.get(c) == "experimental"])}</td>
                    <td style="padding-left: 30px;"><strong>Reference Samples:</strong></td>
                    <td>{len([c for c in sample_cols if sample_types.get(c) == "reference"])}</td>
                    <td style="padding-left: 30px;"><strong>QC Samples:</strong></td>
                    <td>{len([c for c in sample_cols if sample_types.get(c) == "qc"])}</td>
                </tr>
                <tr>
                    <td><strong>Transitions:</strong></td>
                    <td>{stats.get("n_transitions", "N/A") if stats else "N/A"}</td>
                    <td style="padding-left: 30px;"><strong>Peptides:</strong></td>
                    <td>{len(peptide_corrected):,}</td>
                    <td style="padding-left: 30px;"><strong>Proteins:</strong></td>
                    <td>{len(protein_corrected):,}</td>
                    <td></td>
                    <td></td>
                </tr>
            </table>
        </div>

        <div class="summary-box" style="background: #f0fff0; border-color: #90ee90;">
            <h3 style="margin-top: 0; color: #228b22;">Processing Summary</h3>
            <ol style="margin: 10px 0; padding-left: 20px;">
                {"".join(f"<li>{step}</li>" for step in method_log) if method_log else "<li>No processing steps recorded</li>"}
            </ol>
        </div>

        {metrics_html}

        <div class="section-header">
            <h2 style="margin: 0; color: white;">Peptide-Level QC</h2>
        </div>

        {peptide_plots_html if peptide_plots_html else "<p>No peptide-level plots generated.</p>"}

        <div class="section-header">
            <h2 style="margin: 0; color: white;">Protein-Level QC</h2>
        </div>

        {protein_plots_html if protein_plots_html else "<p>No protein-level plots generated.</p>"}

        <div class="timestamp">
            Report generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
        </div>
    </div>
</body>
</html>"""

    # Write the HTML file
    with open(output_path, "w") as f:
        f.write(html)

    logger.info(f"QC report saved to {output_path}")
    if save_plots and plot_paths:
        logger.info(f"QC plots saved to {plots_dir}")

    return plot_paths
