import base64
from pathlib import Path
from typing import Dict

import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.patches import FancyBboxPatch


class GraphGenerator:
    def __init__(self, charts_dir: str = "graphs"):
        """Initialize the GraphGenerator with output directory for charts"""
        self.charts_dir = Path(charts_dir)
        self.charts_dir.mkdir(exist_ok=True)

    def _image_to_base64(self, image_path: Path) -> str:
        """Convert image to base64 string"""
        try:
            with open(image_path, "rb") as image_file:
                return base64.b64encode(image_file.read()).decode("utf-8")
        except FileNotFoundError:
            return ""

    def generate_respiratory_chart(
        self, df: pd.DataFrame, save_as_base64: bool = False
    ) -> str:
        """Generate respiratory chart showing VT and Speed over time"""
        # Get phase times for background regions
        first_unique_phase = df.drop_duplicates(subset="PHASE")
        phase_times = first_unique_phase["T(sec)"].tolist()

        plt.figure(figsize=(18, 5))
        ax1 = plt.subplot()

        # Plot VT with step-like appearance
        sns.lineplot(data=df, x="T(sec)", y="VT(l)_smoothed", label="VT (L)")
        ax1.set_xlabel("Time (sec)")
        ax1.set_ylabel("VT (L)")
        ax1.grid(True, alpha=0.1)
        ax1.set_ylim(0, min(8, df["VT(l)_smoothed"].max()))

        # Plot speed as step function on secondary y-axis
        ax2 = ax1.twinx()
        ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
        line2 = sns.lineplot(
            data=df,
            x="T(sec)",
            y="Speed",
            color="green",
            ax=ax2,
            drawstyle="steps-post",
            linewidth=2,
            label="Speed",
        )
        ax2.set_ylabel("Speed")
        ax2.set_ylim(0, min(30, df["Speed"].max()) + 1)

        # Remove default legends first
        ax1.get_legend().remove()
        ax2.get_legend().remove()

        # Combine legends from both axes in the top left
        lines1, labels1 = ax1.get_legend_handles_labels()
        lines2, labels2 = ax2.get_legend_handles_labels()
        ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")

        # Add colored background regions
        if len(phase_times) >= 4:
            ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
            ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
            ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
            ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")

        chart_path = self.charts_dir / "respiratory.png"
        plt.savefig(chart_path, dpi=300, bbox_inches="tight")
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_fuel_utilization_chart(
        self, df: pd.DataFrame, save_as_base64: bool = False
    ) -> str:
        """Generate fuel utilization chart with stacked bars showing fat vs carbs"""
        # Group by speed and calculate mean for numeric columns only
        speed_groups = df.groupby("Speed").mean(numeric_only=True).round(1)
        speed_groups = speed_groups.iloc[1:-1]
        filtered_data = speed_groups[
            (speed_groups.index >= 3.5) & (speed_groups.index <= 7.5)
        ]

        plt.figure(figsize=(15, 8))
        plt.style.use("default")

        # Create stage labels and positions
        stage_labels = [f"Stage {i}" for i in range(1, len(filtered_data) + 1)]
        x_positions = np.arange(len(filtered_data))

        # Calculate fat and carbs energy expenditure from percentages
        fat_ee = filtered_data["EE(kcal/min)"] * filtered_data["FAT(%)"] / 100
        carbs_ee = filtered_data["EE(kcal/min)"] * filtered_data["CARBS(%)"] / 100

        # Create the main axis for the stacked bars
        ax1 = plt.gca()

        # Create stacked bar chart with colors
        ax1.bar(x_positions, fat_ee, color="#1f77b4", alpha=0.8, width=0.6, label="Fat")
        ax1.bar(
            x_positions,
            carbs_ee,
            bottom=fat_ee,
            color="#ff7f0e",
            alpha=0.8,
            width=0.6,
            label="Carbs",
        )

        # Set labels and formatting for primary axis
        ax1.set_xlabel("", fontsize=12)
        ax1.set_ylabel("Fuel (kcal/min)", fontsize=12)
        ax1.set_ylim(0, 20)

        # Add individual values on each bar segment
        for i, (fat_val, carb_val, total_val) in enumerate(
            zip(fat_ee, carbs_ee, filtered_data["EE(kcal/min)"])
        ):
            if fat_val > 0.3:  # Fat value
                ax1.text(
                    i,
                    fat_val / 2,
                    f"{fat_val:.1f}",
                    ha="center",
                    va="center",
                    fontsize=9,
                    fontweight="bold",
                    color="white",
                )
            if carb_val > 0.3:  # Carbs value
                ax1.text(
                    i,
                    fat_val + carb_val / 2,
                    f"{carb_val:.1f}",
                    ha="center",
                    va="center",
                    fontsize=9,
                    fontweight="bold",
                    color="white",
                )
            # Total EE
            ax1.text(
                i,
                total_val + 0.5,
                f"{total_val:.1f} kcal",
                ha="center",
                va="bottom",
                fontsize=10,
                fontweight="bold",
                color="black",
            )

        # Add speed labels below x-axis
        for i, speed in enumerate(filtered_data.index):
            ax1.text(i, -1.5, f"{speed:.1f} mph", ha="center", va="top", fontsize=9)
            ax1.text(
                i,
                -2.8,
                f"{speed * 1.609:.1f} min/km",
                ha="center",
                va="top",
                fontsize=8,
                color="gray",
            )

        # Create secondary y-axis for heart rate
        ax2 = ax1.twinx()

        # Plot heart rate line
        ax2.plot(
            x_positions,
            filtered_data["HR(bpm)"],
            marker="o",
            linewidth=3,
            markersize=8,
            color="red",
            label="Heart Rate",
        )

        # Set heart rate axis formatting
        ax2.set_ylabel("Heart Rate (bpm)", fontsize=12, color="red")
        ax2.tick_params(axis="y", labelcolor="red")
        ax2.set_ylim(0, 220)

        # Add HR values above the points
        for i, hr in enumerate(filtered_data["HR(bpm)"]):
            ax2.text(
                i,
                hr + 10,
                f"{int(hr)}bpm",
                ha="center",
                va="bottom",
                fontsize=10,
                fontweight="bold",
                color="red",
            )

        # Set x-axis formatting
        ax1.set_xticks(x_positions)
        ax1.set_xticklabels(stage_labels, fontsize=11)

        # Create legend
        lines1, labels1 = ax1.get_legend_handles_labels()
        lines2, labels2 = ax2.get_legend_handles_labels()
        ax1.legend(
            lines1 + lines2,
            labels1 + labels2,
            loc="upper left",
            frameon=True,
            fancybox=True,
            shadow=True,
        )

        # Add grid
        ax1.grid(True, alpha=0.3, linestyle="-", linewidth=0.5)
        ax1.set_axisbelow(True)

        # Adjust layout
        plt.tight_layout()
        plt.subplots_adjust(bottom=0.1, top=0.9)

        chart_path = self.charts_dir / "fuel_utilization_chart.png"
        plt.savefig(chart_path, dpi=300)
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_vo2_pulse_chart(
        self, df: pd.DataFrame, save_as_base64: bool = False
    ) -> str:
        """Generate VO2 Pulse chart with heart rate and speed"""
        first_unique_phase = df.drop_duplicates(subset="PHASE")
        phase_times = first_unique_phase["T(sec)"].tolist()

        plt.figure(figsize=(18, 5))
        ax1 = plt.subplot()

        # Plot VO2 Pulse
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="VO2 Pulse_smoothed",
            label="VO2 Pulse (mL/beat)",
            color="blue",
        )
        ax1.set_xlabel("Time (sec)")
        ax1.set_ylabel("VO2 Pulse (mL/beat)")
        ax1.set_ylim(0, df["VO2 Pulse_smoothed"].max())
        ax1.grid(True, alpha=0.1)

        # Create second y-axis for heart rate
        ax2 = ax1.twinx()
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="HR(bpm)_smoothed",
            color="red",
            ax=ax2,
            linewidth=2,
            label="Heart Rate (bpm)",
        )
        ax2.set_ylabel("Heart Rate (bpm)", color="red")
        ax2.tick_params(axis="y", labelcolor="red")
        ax2.set_ylim(0, df["HR(bpm)_smoothed"].max() + 1)

        # Create third y-axis for speed
        ax3 = ax1.twinx()
        ax3.spines["right"].set_position(("outward", 60))
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="Speed",
            color="green",
            ax=ax3,
            drawstyle="steps-post",
            linewidth=2,
            label="Speed",
        )
        ax3.set_ylabel("Speed", color="green")
        ax3.tick_params(axis="y", labelcolor="green")
        ax3.set_ylim(0, df["Speed"].max() + 1)

        ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))

        # Remove default legends first
        for ax in [ax1, ax2, ax3]:
            if ax.get_legend():
                ax.get_legend().remove()

        # Combine legends from all axes
        lines1, labels1 = ax1.get_legend_handles_labels()
        lines2, labels2 = ax2.get_legend_handles_labels()
        lines3, labels3 = ax3.get_legend_handles_labels()
        ax1.legend(
            lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc="upper left"
        )

        # Add colored background regions
        if len(phase_times) >= 4:
            ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
            ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
            ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
            ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")

        chart_path = self.charts_dir / "vo2_pulse_chart.png"
        plt.savefig(chart_path, bbox_inches="tight", dpi=300)
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_vo2_breath_chart(
        self, df: pd.DataFrame, save_as_base64: bool = False
    ) -> str:
        """Generate VO2 per Breath chart"""
        first_unique_phase = df.drop_duplicates(subset="PHASE")
        phase_times = first_unique_phase["T(sec)"].tolist()

        plt.figure(figsize=(18, 5))
        ax1 = plt.subplot()

        # Plot VO2 per Breath
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="VO2 Breath_smoothed",
            label="VO2 per Breath (mL/breath)",
        )
        ax1.set_xlabel("Time (sec)")
        ax1.set_ylabel("VO2 per Breath (mL/breath)")
        ax1.set_ylim(0, df["VO2 Breath_smoothed"].max() + 1)
        ax1.grid(True, alpha=0.1)

        # Plot speed as step function on secondary y-axis
        ax2 = ax1.twinx()
        ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="Speed",
            color="green",
            ax=ax2,
            drawstyle="steps-post",
            linewidth=2,
            label="Speed",
        )
        ax2.set_ylim(0, df["Speed"].max() + 1)
        ax2.set_ylabel("Speed")

        # Remove default legends first
        ax1.get_legend().remove()
        ax2.get_legend().remove()

        # Combine legends from both axes in the top left
        lines1, labels1 = ax1.get_legend_handles_labels()
        lines2, labels2 = ax2.get_legend_handles_labels()
        ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")

        # Add colored background regions
        if len(phase_times) >= 4:
            ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
            ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
            ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
            ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")

        chart_path = self.charts_dir / "vo2_breath_chart.png"
        plt.savefig(chart_path, bbox_inches="tight", dpi=300)
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_fat_metabolism_chart(
        self, df: pd.DataFrame, save_as_base64: bool = False
    ) -> str:
        """Generate CHO and FAT metabolism chart"""
        first_unique_phase = df.drop_duplicates(subset="PHASE")
        phase_times = first_unique_phase["T(sec)"].tolist()

        plt.figure(figsize=(18, 5))
        ax1 = plt.subplot()

        # Plot CHO
        sns.lineplot(data=df, x="T(sec)", y="CHO_smoothed", label="CHO (kcal/min)")
        ax1.set_xlabel("Time (sec)")
        ax1.set_ylabel("CHO (kcal/min)")
        ax1.grid(True, alpha=0.1)

        # Plot FAT on secondary y-axis
        ax2 = ax1.twinx()
        ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="FAT_smoothed",
            color="green",
            ax=ax2,
            label="FAT (kcal/min)",
        )
        ax2.set_ylabel("FAT (kcal/min)")
        ax2.set_ylim(0, 15)

        # Remove default legends first
        ax1.get_legend().remove()
        ax2.get_legend().remove()

        # Combine legends from both axes in the top left
        lines1, labels1 = ax1.get_legend_handles_labels()
        lines2, labels2 = ax2.get_legend_handles_labels()
        ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left")

        # Add colored background regions
        if len(phase_times) >= 4:
            ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
            ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
            ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
            ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")

        chart_path = self.charts_dir / "fat_metabolism_chart.png"
        plt.savefig(chart_path, bbox_inches="tight", dpi=300)
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_recovery_chart(
        self, df: pd.DataFrame, save_as_base64: bool = False
    ) -> str:
        """Generate recovery chart with VCO2, HR, and BF"""
        first_unique_phase = df.drop_duplicates(subset="PHASE")
        phase_times = first_unique_phase["T(sec)"].tolist()

        plt.figure(figsize=(18, 5))
        ax1 = plt.subplot()

        # Plot VCO2
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="VCO2(ml/min)_smoothed",
            label="VCO2 (ml/min)",
            color="blue",
        )
        ax1.set_xlabel("Time (sec)")
        ax1.set_ylabel("VCO2 (ml/min)")
        ax1.set_ylim(0, df["VCO2(ml/min)"].max())
        ax1.grid(True, alpha=0.1)

        # Create second y-axis for heart rate
        ax2 = ax1.twinx()
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="HR(bpm)_smoothed",
            color="red",
            ax=ax2,
            linewidth=2,
            label="Heart Rate (bpm)",
        )
        ax2.set_ylabel("Heart Rate (bpm)", color="red")
        ax2.set_ylim(df["HR(bpm)_smoothed"].min(), df["HR(bpm)_smoothed"].max() + 1)
        ax2.tick_params(axis="y", labelcolor="red")

        # Create third y-axis for breathing frequency
        ax3 = ax1.twinx()
        ax3.spines["right"].set_position(("outward", 60))
        sns.lineplot(
            data=df,
            x="T(sec)",
            y="BF(bpm)_smoothed",
            color="green",
            ax=ax3,
            linewidth=2,
            label="BF (bpm)",
        )
        ax3.set_ylabel("BF (bpm)", color="green")
        ax3.tick_params(axis="y", labelcolor="green")
        ax3.set_ylim(0, df["BF(bpm)_smoothed"].max() + 1)
        ax1.set_xticks(np.arange(0, df["T(sec)"].max() + 200, 200))

        # Remove default legends first
        for ax in [ax1, ax2, ax3]:
            if ax.get_legend():
                ax.get_legend().remove()

        # Combine legends from all axes in the top left
        lines1, labels1 = ax1.get_legend_handles_labels()
        lines2, labels2 = ax2.get_legend_handles_labels()
        lines3, labels3 = ax3.get_legend_handles_labels()
        ax1.legend(
            lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc="upper left"
        )

        # Add colored background regions
        if len(phase_times) >= 4:
            ax1.axvspan(0, phase_times[1], alpha=0.2, color="lightblue")
            ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color="purple")
            ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color="lightgreen")
            ax1.axvspan(phase_times[3], df["T(sec)"].max(), alpha=0.2, color="blue")

        chart_path = self.charts_dir / "recovery_chart.png"
        plt.savefig(chart_path, bbox_inches="tight", dpi=300)
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_body_fat_percentage_chart(
        self,
        gender: str,
        age: int,
        body_fat_percentage: float,
        save_as_base64: bool = False,
    ) -> str:
        """Generate body fat percentage chart with ranges"""
        # Define the segments with muted colors
        segments = [
            ("#F8A8A8", 0, 15),  # Muted Red/Salmon: 0% to 15%
            ("#FFEECC", 15, 5),  # Pale Yellow/Cream: 15% to 20%
            ("#D0F0C0", 20, 15),  # Pale Green/Mint: 20% to 35%
            ("#FFEECC", 35, 5),  # Pale Yellow/Cream: 35% to 40%
            ("#F8A8A8", 40, 10),  # Muted Red/Salmon: 40% to 50%
        ]

        # Determine age group
        if 20 <= age <= 39:
            age_group = "20-39"
        elif 40 <= age <= 59:
            age_group = "40-59"
        elif 60 <= age <= 79:
            age_group = "60-79"
        else:
            age_group = "N/A"

        demographic = f"{age_group}\n({gender[0].upper()})"

        fig, ax = plt.subplots(figsize=(10, 2))

        # Create the Segmented Bar
        for color, start, length in segments:
            ax.barh(
                y=0,
                width=length,
                left=start,
                height=1,
                color=color,
                edgecolor="black",
                linewidth=0.5,
            )

        # Add the Indicator (Triangle)
        ax.plot(
            body_fat_percentage,
            1.05,
            marker="v",
            color="black",
            markersize=10,
            clip_on=False,
            transform=ax.get_xaxis_transform(),
        )

        # Set Axis Properties and Labels
        ax.set_xlim(0, 50)
        ax.set_xticks(range(0, 51, 5))
        ax.set_yticks([])
        ax.text(
            -0.05,
            0,
            demographic,
            transform=ax.get_yaxis_transform(),
            va="center",
            ha="right",
            fontsize=12,
        )

        ax.set_xlim(0, 50)
        ticks = range(0, 51, 5)
        ax.set_xticks(ticks)
        labels = [f"{t}%" for t in ticks]
        ax.set_xticklabels(labels)

        # Clean up spines and add small ticks
        ax.spines["right"].set_visible(False)
        ax.spines["top"].set_visible(False)
        ax.spines["left"].set_visible(False)
        ax.spines["bottom"].set_visible(True)

        for x in range(0, 51, 5):
            ax.plot(
                [x, x],
                [-0.05, -0.01],
                color="black",
                transform=ax.get_xaxis_transform(),
                clip_on=False,
            )

        plt.tight_layout()

        chart_path = self.charts_dir / "body_fat_percentage_chart.png"
        plt.savefig(chart_path, bbox_inches="tight", dpi=300)
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_body_composition_chart(
        self, fat_mass_lbs: float, lean_mass_lbs: float, save_as_base64: bool = False
    ) -> str:
        """Generate donut chart for body composition"""
        # Calculate percentages
        total_weight = fat_mass_lbs + lean_mass_lbs
        fat_percentage = (fat_mass_lbs / total_weight) * 100
        lean_percentage = (lean_mass_lbs / total_weight) * 100

        # Data for the chart
        sizes = [fat_percentage, lean_percentage]
        colors = ["#fde3ac", "#ff9966"]  # Light yellow/tan and orange

        plt.figure(figsize=(8, 8))

        # Create the donut chart without labels first
        wedges, texts, autotexts = plt.pie(
            sizes,
            autopct="",  # Remove auto percentages
            startangle=90,
            wedgeprops=dict(width=0.5, edgecolor="w"),
            colors=colors,
            labels=["", ""],
        )  # Remove default labels

        # Add custom text annotations positioned manually
        plt.text(
            -1,
            1,
            f"Fat Mass ({fat_mass_lbs:.1f}lbs)\n{fat_percentage:.1f}%",
            fontsize=14,
            fontweight="bold",
            ha="center",
            va="center",
            bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8),
        )

        plt.text(
            1,
            -1,
            f"Lean Mass ({lean_mass_lbs:.1f}lbs)\n{lean_percentage:.1f}%",
            fontsize=14,
            fontweight="bold",
            ha="center",
            va="center",
            bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8),
        )

        # Set the title
        plt.axis("equal")  # Equal aspect ratio ensures that pie is drawn as a circle

        chart_path = self.charts_dir / "body_composition_chart.png"
        plt.savefig(chart_path, bbox_inches="tight", dpi=600)
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_spirometry_chart(
        self, spirometry_df: pd.DataFrame, save_as_base64: bool = False
    ) -> str:
        """Generate spirometry chart with Z-scores and ranges"""
        # Coerce numeric columns
        for col in ["Best", "LLN", "Pred.", "%Pred.", "ZScore"]:
            if col in spirometry_df.columns:
                spirometry_df[col] = pd.to_numeric(spirometry_df[col], errors="coerce")

        # Select rows of interest and prepare display values
        rows_map = {
            "Lung Volume": "FVC",
            "Lung Power": "FEV1",
            "Power/Volume": "FEV1/FVC%",
        }

        records = []
        for label, param in rows_map.items():
            row = spirometry_df.loc[spirometry_df["Parameters"].str.strip() == param]
            if row.empty:
                continue
            row = row.iloc[0]
            records.append(
                {
                    "label": label,
                    "param": param,
                    "best": row["Best"],
                    "pct": row["%Pred."],
                    "z": row["ZScore"],
                }
            )

        # Figure setup
        fig, axes = plt.subplots(
            nrows=3,
            ncols=1,
            figsize=(11.5, 3.6),
            sharex=True,
            gridspec_kw={"hspace": 0.65},
        )

        x_min, x_max = -5, 3
        # Segment colors: red -> orange -> yellow -> green
        segments = [
            (-5, -4, "#f4a7a7"),  # red-ish
            (-4, -3, "#f7c49a"),  # orange-ish
            (-3, -1.7, "#f6e3a3"),  # yellow-ish
            (-1.7, 3, "#c9f0cc"),  # green-ish
        ]

        ticks = np.arange(x_min, x_max + 1, 1)
        labels = [str(i) for i in ticks]

        # Plot each row
        for ax, rec in zip(axes, records):
            # Background segments
            for a, b, color in segments:
                ax.barh(
                    0, width=b - a, left=a, height=0.6, color=color, edgecolor="none"
                )

            # LLN (-1) and Predicted (0) markers
            ax.axvline(0, color="black", lw=1)

            # Z-score pointer (downward triangle) at top of each panel
            if pd.notna(rec["z"]):
                trans = mtransforms.blended_transform_factory(
                    ax.transData, ax.transAxes
                )
                ax.plot(
                    float(rec["z"]),
                    1.2,
                    marker="v",
                    markersize=12,
                    color="dimgray",
                    transform=trans,
                    clip_on=False,
                )

            # Labels, ticks, and styling
            ax.set_title(
                rec["label"], loc="left", fontsize=11, fontweight="bold", pad=2
            )
            ax.set_xlim(x_min, x_max)
            ax.set_yticks([])
            ax.set_xticks(ticks)
            ax.set_xticklabels(labels, fontsize=8)
            ax.set_xlabel("")

        # Top annotations
        axes[0].text(-1.7, 0.45, "LLN", ha="center", va="bottom", fontsize=9)
        axes[0].text(0, 0.45, "Predicted", ha="center", va="bottom", fontsize=9)

        # Right-side summary boxes
        fig.subplots_adjust(right=0.78)
        box_ax = fig.add_axes(
            [0.805, 0.06, 0.18, 0.90]
        )  # [left, bottom, width, height]
        box_ax.axis("off")

        # Helper to draw a pill-shaped text box
        def pill(ax, xy, text):
            x, y = xy
            # Draw rounded rectangle background
            bbox = FancyBboxPatch(
                (x - 0.48, y - 0.09),
                0.96,
                0.18,
                boxstyle="round,pad=0.02,rounding_size=0.08",
                ec="#dddddd",
                fc="#f3f3f3",
                linewidth=1.0,
            )
            ax.add_patch(bbox)
            ax.text(
                x,
                y + 0.025,
                text,
                ha="center",
                va="center",
                fontsize=11,
                fontweight="bold",
            )
            ax.text(
                x,
                y - 0.055,
                "of predicted",
                ha="center",
                va="center",
                fontsize=9,
                color="#555555",
            )

        box_ax.set_xlim(0, 1)
        box_ax.set_ylim(0, 1)

        # Prepare display strings and positions (top to bottom)
        right_items = []
        for rec in records:
            name = (
                "FVC"
                if rec["param"] == "FVC"
                else ("FEV1" if rec["param"] == "FEV1" else "FEV1/FVC")
            )
            unit = "L" if rec["param"] in ("FVC", "FEV1") else "%"
            value_fmt = f"{rec['best']:.2f}{unit}"
            pct_fmt = f"{rec['pct']:.1f}%"
            right_items.append((name, value_fmt, pct_fmt))

        # Sort to match image order on the right (FVC, FEV1, FEV1/FVC)
        order = ["FVC", "FEV1", "FEV1/FVC"]
        right_items_sorted = [
            next(item for item in right_items if item[0] == k) for k in order
        ]

        ys = [0.82, 0.48, 0.15]
        for (name, value_fmt, pct_fmt), y in zip(right_items_sorted, ys):
            main_line = f"{name}\n{value_fmt} → {pct_fmt}"
            pill(box_ax, (0.5, y), main_line)

        chart_path = self.charts_dir / "spirometry_chart.png"
        plt.savefig(chart_path, dpi=300, bbox_inches="tight")
        plt.close()

        return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)

    def generate_all_charts(
        self,
        pnoe_df: pd.DataFrame,
        spirometry_df: pd.DataFrame,
        patient_data: Dict,
        save_as_base64: bool = False,
    ) -> Dict[str, str]:
        """Generate all charts at once and return dictionary of paths/base64 strings"""
        charts = {}

        # Generate physiological charts
        charts["respiratory"] = self.generate_respiratory_chart(pnoe_df, save_as_base64)
        charts["fuel_utilization_chart"] = self.generate_fuel_utilization_chart(
            pnoe_df, save_as_base64
        )
        charts["vo2_pulse_chart"] = self.generate_vo2_pulse_chart(
            pnoe_df, save_as_base64
        )
        charts["vo2_breath_chart"] = self.generate_vo2_breath_chart(
            pnoe_df, save_as_base64
        )
        charts["fat_metabolism_chart"] = self.generate_fat_metabolism_chart(
            pnoe_df, save_as_base64
        )
        charts["recovery_chart"] = self.generate_recovery_chart(pnoe_df, save_as_base64)

        # Generate body composition charts
        if (
            "gender" in patient_data
            and "age" in patient_data
            and "fat_percentage" in patient_data
        ):
            charts["body_fat_percentage_chart"] = (
                self.generate_body_fat_percentage_chart(
                    patient_data["gender"],
                    patient_data["age"],
                    patient_data["fat_percentage"],
                    save_as_base64,
                )
            )

        if "fat_mass_lbs" in patient_data and "lean_mass_lbs" in patient_data:
            charts["body_composition_chart"] = self.generate_body_composition_chart(
                patient_data["fat_mass_lbs"],
                patient_data["lean_mass_lbs"],
                save_as_base64,
            )

        # Generate spirometry chart
        charts["spirometry_chart"] = self.generate_spirometry_chart(
            spirometry_df, save_as_base64
        )

        return charts


# Example usage
if __name__ == "__main__":
    # Initialize graph generator
    generator = GraphGenerator()

    # Load sample data (you would pass your actual dataframes)
    pnoe_df = pd.read_csv("data/Pnoe_20250729_1550-Moran_Keirstyn.csv", delimiter=";")
    spirometry_df = pd.read_csv("data/spirometry_data.csv")

    # Preprocess pnoe data (same as in your notebook)
    pnoe_df = pnoe_df.apply(pd.to_numeric, errors="ignore")
    pnoe_df["VO2 Pulse"] = pnoe_df["VO2(ml/min)"] / pnoe_df["HR(bpm)"]
    pnoe_df["VO2 Breath"] = pnoe_df["VO2(ml/min)"] / pnoe_df["BF(bpm)"]
    pnoe_df["CHO"] = pnoe_df["EE(kcal/min)"] * pnoe_df["CARBS(%)"] / 100
    pnoe_df["FAT"] = pnoe_df["EE(kcal/min)"] * pnoe_df["FAT(%)"] / 100

    # Apply smoothing
    window_size = 10
    columns_to_smooth = [
        "VO2(ml/min)",
        "VCO2(ml/min)",
        "HR(bpm)",
        "VT(l)",
        "BF(bpm)",
        "VE(l/min)",
        "VO2 Pulse",
        "VO2 Breath",
        "CHO",
        "FAT",
    ]
    for col in columns_to_smooth:
        if col in pnoe_df.columns:
            pnoe_df[f"{col}_smoothed"] = (
                pnoe_df[col].rolling(window=window_size, min_periods=1).mean()
            )

    # Patient data
    patient_data = {
        "gender": "female",
        "age": 25,
        "fat_percentage": 22.4,
        "fat_mass_lbs": 27.6,
        "lean_mass_lbs": 95.4,
    }

    # Generate all charts
    charts = generator.generate_all_charts(
        pnoe_df, spirometry_df, patient_data, save_as_base64=True
    )

    print(f"Generated {len(charts)} charts:")
    for chart_name in charts.keys():
        print(f"- {chart_name}")
