"""
Gate Validator Service
======================
Valida resultados de backtesting contra umbrales del TRADING-STRATEGIST.
Implementa el "Gate" que debe pasarse antes de ir a produccion.

Referencia: PERFIL-TRADING-STRATEGIST.md + validation_oos.yaml
Creado: 2026-01-04
Autor: ML-Specialist (NEXUS v4.0)
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from datetime import datetime
from enum import Enum
import yaml
from pathlib import Path
from loguru import logger


class GateStatus(Enum):
    """Estados posibles del gate."""
    PASSED = "passed"
    FAILED = "failed"
    WARNING = "warning"
    PENDING = "pending"


@dataclass
class MetricValidation:
    """Validacion de una metrica individual."""
    name: str
    value: float
    threshold: float
    operator: str  # 'gte', 'lte', 'gt', 'lt', 'eq'
    passed: bool
    margin: float = 0.0  # Porcentaje sobre/debajo del umbral

    def to_dict(self) -> Dict:
        return {
            "name": self.name,
            "value": self.value,
            "threshold": self.threshold,
            "operator": self.operator,
            "passed": self.passed,
            "margin": round(self.margin * 100, 2)
        }


@dataclass
class GateValidationResult:
    """Resultado completo de validacion del gate."""
    status: GateStatus
    symbol: str
    validation_date: datetime
    metrics_validated: List[MetricValidation]
    overfitting_check: Optional[Dict] = None
    summary: Dict = field(default_factory=dict)
    recommendations: List[str] = field(default_factory=list)

    @property
    def all_passed(self) -> bool:
        return all(m.passed for m in self.metrics_validated)

    @property
    def critical_passed(self) -> bool:
        critical_metrics = ['sharpe_ratio', 'max_drawdown', 'profit_factor']
        critical = [m for m in self.metrics_validated if m.name in critical_metrics]
        return all(m.passed for m in critical)

    def to_dict(self) -> Dict:
        return {
            "status": self.status.value,
            "symbol": self.symbol,
            "validation_date": self.validation_date.isoformat(),
            "all_passed": self.all_passed,
            "critical_passed": self.critical_passed,
            "metrics": [m.to_dict() for m in self.metrics_validated],
            "overfitting_check": self.overfitting_check,
            "summary": self.summary,
            "recommendations": self.recommendations
        }


class GateValidator:
    """
    Validador del Gate de Trading.

    Verifica que las metricas de backtesting cumplan con los
    umbrales definidos por el TRADING-STRATEGIST antes de
    permitir operaciones en produccion.
    """

    # Umbrales por defecto (TRADING-STRATEGIST)
    DEFAULT_THRESHOLDS = {
        "sharpe_ratio": {"value": 1.0, "operator": "gte"},
        "sortino_ratio": {"value": 1.5, "operator": "gte"},
        "calmar_ratio": {"value": 1.0, "operator": "gte"},
        "max_drawdown": {"value": 0.20, "operator": "lte"},
        "win_rate": {"value": 0.40, "operator": "gte"},
        "profit_factor": {"value": 1.5, "operator": "gte"},
    }

    # Umbral para detectar overfitting
    OOS_DEGRADATION_THRESHOLD = 0.30  # 30% degradacion maxima permitida

    def __init__(self, config_path: Optional[str] = None):
        """
        Inicializa el validador.

        Args:
            config_path: Ruta a archivo de configuracion YAML (opcional)
        """
        self.thresholds = self.DEFAULT_THRESHOLDS.copy()

        if config_path:
            self._load_config(config_path)

        logger.info("GateValidator initialized")

    def _load_config(self, config_path: str):
        """Carga umbrales desde archivo YAML."""
        try:
            with open(config_path, 'r') as f:
                config = yaml.safe_load(f)

            if 'metrics_thresholds' in config:
                thresholds = config['metrics_thresholds']

                # Mapear nombres de config a nombres internos
                mapping = {
                    'sharpe_ratio_min': ('sharpe_ratio', 'gte'),
                    'sortino_ratio_min': ('sortino_ratio', 'gte'),
                    'calmar_ratio_min': ('calmar_ratio', 'gte'),
                    'max_drawdown_max': ('max_drawdown', 'lte'),
                    'win_rate_min': ('win_rate', 'gte'),
                    'profit_factor_min': ('profit_factor', 'gte'),
                }

                for config_key, (metric_name, operator) in mapping.items():
                    if config_key in thresholds:
                        self.thresholds[metric_name] = {
                            "value": thresholds[config_key],
                            "operator": operator
                        }

                # Cargar umbral de overfitting
                if 'in_sample_vs_oos_threshold' in thresholds:
                    self.OOS_DEGRADATION_THRESHOLD = thresholds['in_sample_vs_oos_threshold']

            logger.info(f"Loaded thresholds from {config_path}")

        except Exception as e:
            logger.warning(f"Could not load config from {config_path}: {e}")

    def _validate_metric(
        self,
        name: str,
        value: float,
        threshold: float,
        operator: str
    ) -> MetricValidation:
        """Valida una metrica individual."""
        operators = {
            'gte': lambda v, t: v >= t,
            'lte': lambda v, t: v <= t,
            'gt': lambda v, t: v > t,
            'lt': lambda v, t: v < t,
            'eq': lambda v, t: abs(v - t) < 0.0001,
        }

        check_fn = operators.get(operator, operators['gte'])
        passed = check_fn(value, threshold)

        # Calcular margen (cuanto sobre/debajo del umbral)
        if threshold != 0:
            if operator in ['gte', 'gt']:
                margin = (value - threshold) / abs(threshold)
            else:  # lte, lt
                margin = (threshold - value) / abs(threshold)
        else:
            margin = 0.0

        return MetricValidation(
            name=name,
            value=value,
            threshold=threshold,
            operator=operator,
            passed=passed,
            margin=margin
        )

    def validate(
        self,
        metrics: Dict[str, float],
        symbol: str,
        in_sample_metrics: Optional[Dict[str, float]] = None
    ) -> GateValidationResult:
        """
        Valida metricas de backtesting contra umbrales.

        Args:
            metrics: Diccionario con metricas del backtest OOS
            symbol: Simbolo evaluado
            in_sample_metrics: Metricas in-sample para detectar overfitting

        Returns:
            GateValidationResult con resultado de validacion
        """
        validations = []
        recommendations = []

        # Mapeo de nombres de metricas
        metric_mapping = {
            'sharpe_ratio': ['sharpe_ratio', 'sharpe'],
            'sortino_ratio': ['sortino_ratio', 'sortino'],
            'calmar_ratio': ['calmar_ratio', 'calmar'],
            'max_drawdown': ['max_drawdown_pct', 'max_drawdown', 'max_dd'],
            'win_rate': ['winrate', 'win_rate'],
            'profit_factor': ['profit_factor', 'pf'],
        }

        # Validar cada metrica
        for metric_name, config in self.thresholds.items():
            # Buscar valor en metricas con diferentes nombres posibles
            value = None
            for possible_name in metric_mapping.get(metric_name, [metric_name]):
                if possible_name in metrics:
                    value = metrics[possible_name]
                    break

            if value is None:
                logger.warning(f"Metric {metric_name} not found in results")
                continue

            # Para max_drawdown, usar valor absoluto
            if metric_name == 'max_drawdown':
                value = abs(value)

            validation = self._validate_metric(
                name=metric_name,
                value=value,
                threshold=config['value'],
                operator=config['operator']
            )
            validations.append(validation)

            # Generar recomendaciones para metricas fallidas
            if not validation.passed:
                if metric_name == 'sharpe_ratio':
                    recommendations.append(
                        f"Mejorar Sharpe Ratio: considerar filtrar trades de baja calidad o ajustar position sizing"
                    )
                elif metric_name == 'max_drawdown':
                    recommendations.append(
                        f"Reducir drawdown: implementar trailing stops o reducir tamano de posicion"
                    )
                elif metric_name == 'profit_factor':
                    recommendations.append(
                        f"Mejorar Profit Factor: revisar ratio risk:reward o mejorar timing de entrada"
                    )
                elif metric_name == 'win_rate':
                    recommendations.append(
                        f"Mejorar Win Rate: ajustar filtros de entrada o mejorar deteccion de fases AMD"
                    )

        # Check de overfitting si tenemos metricas in-sample
        overfitting_check = None
        if in_sample_metrics:
            overfitting_check = self._check_overfitting(metrics, in_sample_metrics)
            if overfitting_check.get('overfitting_detected'):
                recommendations.append(
                    f"Posible overfitting detectado: performance OOS degradado {overfitting_check['degradation_pct']:.1f}% vs in-sample"
                )

        # Determinar status final
        all_passed = all(v.passed for v in validations)
        critical_metrics = ['sharpe_ratio', 'max_drawdown', 'profit_factor']
        critical_validations = [v for v in validations if v.name in critical_metrics]
        critical_passed = all(v.passed for v in critical_validations)

        if all_passed and (overfitting_check is None or not overfitting_check.get('overfitting_detected')):
            status = GateStatus.PASSED
        elif critical_passed:
            status = GateStatus.WARNING
        else:
            status = GateStatus.FAILED

        # Crear resumen
        summary = {
            "total_metrics": len(validations),
            "passed": sum(1 for v in validations if v.passed),
            "failed": sum(1 for v in validations if not v.passed),
            "critical_passed": critical_passed,
            "overfitting_risk": overfitting_check.get('overfitting_detected', False) if overfitting_check else False
        }

        result = GateValidationResult(
            status=status,
            symbol=symbol,
            validation_date=datetime.now(),
            metrics_validated=validations,
            overfitting_check=overfitting_check,
            summary=summary,
            recommendations=recommendations
        )

        logger.info(f"Gate validation for {symbol}: {status.value} ({summary['passed']}/{summary['total_metrics']} passed)")

        return result

    def _check_overfitting(
        self,
        oos_metrics: Dict[str, float],
        is_metrics: Dict[str, float]
    ) -> Dict:
        """
        Detecta posible overfitting comparando metricas OOS vs in-sample.

        Returns:
            Dict con resultado del check de overfitting
        """
        key_metrics = ['sharpe_ratio', 'profit_factor', 'winrate']
        degradations = []

        for metric in key_metrics:
            oos_val = oos_metrics.get(metric, oos_metrics.get(f"{metric}_ratio"))
            is_val = is_metrics.get(metric, is_metrics.get(f"{metric}_ratio"))

            if oos_val is not None and is_val is not None and is_val > 0:
                degradation = 1 - (oos_val / is_val)
                degradations.append(degradation)

        if not degradations:
            return {"overfitting_detected": False, "reason": "No comparable metrics"}

        avg_degradation = sum(degradations) / len(degradations)

        return {
            "overfitting_detected": avg_degradation > self.OOS_DEGRADATION_THRESHOLD,
            "degradation_pct": avg_degradation * 100,
            "threshold_pct": self.OOS_DEGRADATION_THRESHOLD * 100,
            "metrics_compared": len(degradations)
        }

    def generate_report(self, result: GateValidationResult) -> str:
        """Genera reporte legible del resultado."""
        lines = []
        lines.append("=" * 60)
        lines.append(f"GATE VALIDATION REPORT - {result.symbol}")
        lines.append("=" * 60)
        lines.append(f"Date: {result.validation_date.strftime('%Y-%m-%d %H:%M:%S')}")
        lines.append(f"Status: {result.status.value.upper()}")
        lines.append("")

        lines.append("METRICS VALIDATION:")
        lines.append("-" * 60)
        lines.append(f"{'Metric':<20} {'Value':<12} {'Threshold':<12} {'Status':<10}")
        lines.append("-" * 60)

        for m in result.metrics_validated:
            status_str = "PASS" if m.passed else "FAIL"
            op_symbol = ">=" if m.operator in ['gte', 'gt'] else "<="
            lines.append(
                f"{m.name:<20} {m.value:<12.4f} {op_symbol} {m.threshold:<8.4f} {status_str:<10}"
            )

        lines.append("-" * 60)
        lines.append(f"Passed: {result.summary['passed']}/{result.summary['total_metrics']}")
        lines.append("")

        if result.overfitting_check:
            lines.append("OVERFITTING CHECK:")
            lines.append("-" * 60)
            oc = result.overfitting_check
            if oc.get('overfitting_detected'):
                lines.append(f"WARNING: Possible overfitting detected!")
                lines.append(f"OOS degradation: {oc['degradation_pct']:.1f}% (threshold: {oc['threshold_pct']:.0f}%)")
            else:
                lines.append(f"No overfitting detected (degradation: {oc.get('degradation_pct', 0):.1f}%)")
            lines.append("")

        if result.recommendations:
            lines.append("RECOMMENDATIONS:")
            lines.append("-" * 60)
            for i, rec in enumerate(result.recommendations, 1):
                lines.append(f"{i}. {rec}")
            lines.append("")

        lines.append("=" * 60)
        lines.append(f"GATE: {'APPROVED' if result.status == GateStatus.PASSED else 'NOT APPROVED'}")
        lines.append("=" * 60)

        return "\n".join(lines)


# Factory function
def create_gate_validator(config_path: Optional[str] = None) -> GateValidator:
    """Crea instancia de GateValidator."""
    if config_path is None:
        # Usar config por defecto si existe
        default_path = Path(__file__).parent.parent.parent / "config" / "validation_oos.yaml"
        if default_path.exists():
            config_path = str(default_path)

    return GateValidator(config_path)


if __name__ == "__main__":
    # Test del validator
    validator = GateValidator()

    # Metricas de ejemplo
    test_metrics = {
        "sharpe_ratio": 1.23,
        "sortino_ratio": 1.67,
        "max_drawdown_pct": -0.085,
        "winrate": 0.525,
        "profit_factor": 1.85,
    }

    in_sample = {
        "sharpe_ratio": 1.45,
        "sortino_ratio": 1.90,
        "max_drawdown_pct": -0.07,
        "winrate": 0.55,
        "profit_factor": 2.1,
    }

    result = validator.validate(test_metrics, "XAUUSD", in_sample)
    print(validator.generate_report(result))
