# src/backtest/visualizers/walk_forward_visualizer.py

from pathlib import Path
import ast
import json
import re
import pandas as pd
import matplotlib.pyplot as plt
from src.utils.logger import log


class WalkForwardVisualizer:
    """
    Visualizador para resultados de Walk-Forward Validation.

    Input esperado (desde CSVs):
    - summary_df: walkforward_summary.csv
    - windows_df: walkforward_windows.csv  (incluye columna 'params')
    """

    def __init__(
        self,
        summary_df: pd.DataFrame,
        windows_df: pd.DataFrame,
        name: str = "WalkForward",
        output_dir: Path | str = "backtest_results/walkforward/plots",
    ):
        self.summary = summary_df.copy()
        self.windows = windows_df.copy()
        self.name = name

        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)

        self._parse_params_column()

        log.info(f"📊 WalkForwardVisualizer inicializado → {self.output_dir}")

    # ------------------------------------------------------------------
    # 🔧 PARSE ROBUSTO DE PARAMS
    # ------------------------------------------------------------------

    @staticmethod
    def _coerce_params_string_to_dict(s: str):
        """
        Convierte string de params (desde CSV) a dict.
        Soporta:
        - "{'fast_period': 15, ...}"
        - "{'fast_period': np.int64(15), 'use_adx': np.True_, ...}"
        - JSON con comillas dobles
        """
        if s is None:
            return None

        if not isinstance(s, str):
            return None

        raw = s.strip()
        if raw == "" or raw.lower() in {"none", "nan", "null"}:
            return None

        # Limpieza de serializaciones típicas numpy
        cleaned = raw

        # np.int64(15) -> 15
        cleaned = re.sub(r"np\.int64\((\-?\d+)\)", r"\1", cleaned)

        # np.float64(1.23) -> 1.23
        cleaned = re.sub(r"np\.float64\(([-+0-9eE\.]+)\)", r"\1", cleaned)

        # np.True_ / np.False_ -> True/False
        cleaned = cleaned.replace("np.True_", "True").replace("np.False_", "False")

        # np.bool_(True) -> True
        cleaned = re.sub(r"np\.bool_\((True|False)\)", r"\1", cleaned)

        # 1) intentar literal_eval (dict python)
        try:
            d = ast.literal_eval(cleaned)
            if isinstance(d, dict):
                return d
        except Exception:
            pass

        # 2) intentar json (si viniera con comillas dobles)
        try:
            d = json.loads(cleaned)
            if isinstance(d, dict):
                return d
        except Exception:
            pass

        # 3) intento extra: convertir comillas simples a dobles para json
        try:
            maybe_json = cleaned.replace("'", '"')
            d = json.loads(maybe_json)
            if isinstance(d, dict):
                return d
        except Exception:
            return None

        return None

    def _parse_params_column(self):
        """
        Convierte la columna 'params' a dict.
        """
        if "params" not in self.windows.columns:
            log.warning("⚠️ windows_df no tiene columna 'params'. Se omite parseo.")
            return

        self.windows["params"] = self.windows["params"].apply(
            lambda x: x if isinstance(x, dict) else self._coerce_params_string_to_dict(x)
        )

    # ------------------------------------------------------------------
    # 📊 PLOT 1: MÉTRICAS MEDIAS POR CONFIGURACIÓN
    # ------------------------------------------------------------------

    def plot_avg_metrics(self) -> Path:
        df = self.summary.copy()

        fig, ax = plt.subplots(figsize=(10, 6))
        x = range(len(df))

        ax.bar(x, df["avg_return_pct"], label="Avg Return (%)")
        ax.bar(
            x,
            df["avg_sharpe"],
            bottom=df["avg_return_pct"],
            label="Avg Sharpe",
        )

        ax.set_xticks(x)
        ax.set_xticklabels(df["wf_name"])
        ax.set_title(f"Walk-Forward Performance Summary\n{self.name}")
        ax.legend()
        ax.grid(alpha=0.3)

        path = self.output_dir / "wf_avg_metrics.png"
        fig.tight_layout()
        fig.savefig(path, dpi=150)
        plt.close(fig)

        log.success(f"💾 Plot guardado: {path}")
        return path

    # ------------------------------------------------------------------
    # 📈 PLOT 2: RETURNS POR VENTANA (OOS)
    # ------------------------------------------------------------------

    def plot_returns_by_window(self) -> Path:
        df = self.windows.copy()

        if "wf_name" not in df.columns:
            raise ValueError("windows_df debe contener la columna 'wf_name'")

        df["test_start"] = pd.to_datetime(df["test_start"])
        df = df.sort_values(["wf_name", "test_start"])

        fig, ax = plt.subplots(figsize=(12, 6))

        for wf_name, g in df.groupby("wf_name"):
            ax.plot(
                g["test_start"],
                g["return_pct"],
                marker="o",
                label=wf_name,
            )

        ax.axhline(0, color="black", linestyle="--", linewidth=1)
        ax.set_title(f"Walk-Forward OOS Returns by Window\n{self.name}")
        ax.set_xlabel("Test period start")
        ax.set_ylabel("Return (%)")
        ax.legend()
        ax.grid(alpha=0.3)

        path = self.output_dir / "wf_returns_by_window.png"
        fig.tight_layout()
        fig.savefig(path, dpi=150)
        plt.close(fig)

        log.success(f"💾 Plot guardado: {path}")
        return path

    # ------------------------------------------------------------------
    # 📉 PLOT 3: DRAWDOWN POR VENTANA (OOS)
    # ------------------------------------------------------------------

    def plot_drawdown_by_window(self) -> Path:
        df = self.windows.copy()

        required = {"wf_name", "test_start", "max_dd_pct"}
        missing = required - set(df.columns)
        if missing:
            raise ValueError(f"windows_df no contiene columnas requeridas: {missing}")

        df["test_start"] = pd.to_datetime(df["test_start"])
        df = df.sort_values(["wf_name", "test_start"])

        fig, ax = plt.subplots(figsize=(12, 6))

        for wf_name, g in df.groupby("wf_name"):
            ax.plot(
                g["test_start"],
                g["max_dd_pct"],
                marker="o",
                label=wf_name,
            )

        ax.axhline(0, linestyle="--", linewidth=1)
        ax.set_title(f"Walk-Forward OOS Max Drawdown by Window\n{self.name}")
        ax.set_xlabel("Test period start")
        ax.set_ylabel("Max Drawdown (%)")
        ax.legend()
        ax.grid(alpha=0.3)

        path = self.output_dir / "wf_drawdown_by_window.png"
        fig.tight_layout()
        fig.savefig(path, dpi=150)
        plt.close(fig)

        log.success(f"💾 Plot guardado: {path}")
        return path

    # ------------------------------------------------------------------
    # 📊 PLOT 4: DISTRIBUCIÓN DE RETURNS
    # ------------------------------------------------------------------

    def plot_return_distribution(self, bins: int = 20, overlay: bool = True) -> Path:
        df = self.windows.copy().dropna(subset=["return_pct"])

        path = self.output_dir / "wf_return_distribution.png"

        if overlay:
            fig, ax = plt.subplots(figsize=(12, 6))

            for wf_name, g in df.groupby("wf_name"):
                ax.hist(g["return_pct"], bins=bins, alpha=0.5, label=wf_name)

            ax.axvline(0, linestyle="--", linewidth=1)
            ax.set_title(f"Walk-Forward OOS Return Distribution\n{self.name}")
            ax.set_xlabel("Return (%)")
            ax.set_ylabel("Frequency")
            ax.legend()

            fig.tight_layout()
            fig.savefig(path, dpi=150)
            plt.close(fig)

            log.success(f"💾 Plot guardado: {path}")
            return path

        wf_names = sorted(df["wf_name"].unique())
        n = len(wf_names)

        fig, axes = plt.subplots(n, 1, figsize=(12, 4 * n), sharex=True)
        if n == 1:
            axes = [axes]

        for ax, wf_name in zip(axes, wf_names):
            g = df[df["wf_name"] == wf_name]
            ax.hist(g["return_pct"], bins=bins)
            ax.axvline(0, linestyle="--", linewidth=1)
            ax.set_title(wf_name)

        axes[-1].set_xlabel("Return (%)")
        fig.suptitle(f"Walk-Forward OOS Return Distribution\n{self.name}", y=0.98)

        fig.tight_layout()
        fig.savefig(path, dpi=150)
        plt.close(fig)

        log.success(f"💾 Plot guardado: {path}")
        return path

    # ------------------------------------------------------------------
    # 📈 PLOT 5: ESTABILIDAD DE PARÁMETROS
    # ------------------------------------------------------------------

    def plot_parameter_stability(self, param_name: str) -> Path:
        df = self.windows.copy()

        if "params" not in df.columns:
            raise ValueError("windows_df no tiene columna 'params'")

        # Asegurar que params está parseado
        df["params"] = df["params"].apply(
            lambda x: x if isinstance(x, dict) else self._coerce_params_string_to_dict(x)
        )

        df[param_name] = df["params"].apply(
            lambda p: p.get(param_name) if isinstance(p, dict) else None
        )

        df = df.dropna(subset=[param_name])
        if df.empty:
            # Debug útil para ver qué pasa en tu CSV
            sample = self.windows["params"].dropna().head(5).tolist()
            raise ValueError(
                f"No se pudo extraer '{param_name}' desde params.\n"
                f"Ejemplo de params (primeros 5 no-null): {sample}"
            )

        df["test_start"] = pd.to_datetime(df["test_start"])
        df = df.sort_values(["wf_name", "test_start"])

        fig, ax = plt.subplots(figsize=(12, 6))

        for wf_name, g in df.groupby("wf_name"):
            ax.plot(g["test_start"], g[param_name], marker="o", label=wf_name)

        ax.set_title(f"WF Parameter Stability: {param_name}\n{self.name}")
        ax.set_xlabel("Test period start")
        ax.set_ylabel(param_name)
        ax.legend()
        ax.grid(alpha=0.3)

        path = self.output_dir / f"wf_param_stability_{param_name}.png"
        fig.tight_layout()
        fig.savefig(path, dpi=150)
        plt.close(fig)

        log.success(f"💾 Plot guardado: {path}")
        return path
