# src/calibration/risk_inspector.py

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal, Dict, Any

import numpy as np
import pandas as pd

from src.data.storage import StorageManager
from src.utils.logger import log


# ============================================================
# Domain models (pueden moverse luego a models.py)
# ============================================================

StopType = Literal["fixed", "trailing", "atr"]


@dataclass
class StopConfig:
    type: StopType

    # fixed / trailing
    stop_fraction: float | None = None

    # atr
    atr_period: int | None = None
    atr_multiplier: float | None = None


@dataclass
class RiskConfig:
    risk_fraction: float
    max_position_fraction: float


@dataclass
class GlobalRiskRules:
    max_drawdown_pct: float

    # reservados v1+
    daily_loss_limit_pct: float | None = None
    max_consecutive_losses: int | None = None
    cooldown_bars: int | None = None


# ============================================================
# Public entry point
# ============================================================

def inspect_risk_config(
    *,
    storage: StorageManager,
    symbol: str,
    timeframe: str,
    stop: StopConfig,
    risk: RiskConfig,
    rules: GlobalRiskRules,
    account_equity: float,
    data_quality: Dict[str, Any],
) -> Dict[str, Any]:
    """
    Inspecta si una configuración de riesgo + stop es viable
    con los datos disponibles para un símbolo y timeframe.

    No ejecuta estrategias.
    No persiste estado.
    """

    log.info("🔍 Inspecting risk & stop configuration")

    # --------------------------------------------------
    # Load market data (read-only)
    # --------------------------------------------------
    df = storage.load_ohlcv(symbol=symbol, timeframe=timeframe)

    if df is None or df.empty:
        return _fail_result("No OHLCV data available for risk inspection")

    # --------------------------------------------------
    # Run checks
    # --------------------------------------------------
    checks: Dict[str, Any] = {}

    checks["data_compatibility"] = _check_data_compatibility(data_quality)
    checks["stop_availability"] = _check_stop_availability(df, stop)
    checks["stop_sanity"] = _check_stop_sanity(df, stop)
    checks["sizer_feasibility"] = _check_sizer_feasibility(
        df=df,
        stop=stop,
        risk=risk,
        account_equity=account_equity,
    )

    # --------------------------------------------------
    # Aggregate quality
    # --------------------------------------------------
    status = _aggregate_status(checks)

    message = _build_human_message(status, checks)

    series = _compute_position_series(
        df=df,
        stop=stop,
        risk=risk,
        account_equity=account_equity,
    )

    return {
        "valid": status != "fail",
        "status": status,
        "checks": checks,
        "message": message,
        "series": series,
        "config_snapshot": {
            "symbol": symbol,
            "timeframe": timeframe,
            "account_equity": account_equity,
            "stop": stop.__dict__,
            "risk": risk.__dict__,
            "rules": rules.__dict__,
        },
    }

# ============================================================
# Checks
# ============================================================

def _check_data_compatibility(data_quality: Dict[str, Any]) -> Dict[str, Any]:
    """
    Evalúa si la calidad de datos permite usar stops con fiabilidad.
    """

    status = data_quality.get("status")

    if status == "fail":
        return {
            "status": "fail",
            "message": "Data quality is insufficient for risk calibration",
        }

    gaps = data_quality.get("gaps", {}).get("count", 0)

    if gaps > 0:
        return {
            "status": "warning",
            "message": (
                "Data contains gaps. Stops are evaluated on close price "
                "and may trigger late."
            ),
        }

    return {
        "status": "ok",
        "message": "Data quality is compatible with stop evaluation",
    }


def _check_stop_availability(df: pd.DataFrame, stop: StopConfig) -> Dict[str, Any]:
    """
    Comprueba que el stop puede calcularse con los datos disponibles.
    """

    if stop.type in ("fixed", "trailing"):
        return {"status": "ok"}

    if stop.type == "atr":
        if stop.atr_period is None or stop.atr_multiplier is None:
            return {
                "status": "fail",
                "reason": "atr_parameters_missing",
            }

        if len(df) < stop.atr_period + 5:
            return {
                "status": "fail",
                "reason": "not_enough_candles_for_atr",
            }

        required_cols = {"high", "low", "close"}
        if not required_cols.issubset(df.columns):
            return {
                "status": "fail",
                "reason": "missing_ohlc_columns",
            }

        return {"status": "ok"}

    return {"status": "fail", "reason": "unknown_stop_type"}


def _check_stop_sanity(df: pd.DataFrame, stop: StopConfig) -> Dict[str, Any]:
    """
    Evalúa si el stop tiene una magnitud razonable.
    """

    stop_distances = _compute_stop_distances(df, stop)

    if stop_distances is None or len(stop_distances) == 0:
        return {
            "status": "fail",
            "message": "Unable to compute stop distances",
        }

    quantiles = np.quantile(stop_distances, [0.5, 0.9, 0.99])

    p50, p90, p99 = quantiles

    status = "ok"
    message = "Stop distance looks reasonable"

    if p50 < 0.002:
        status = "warning"
        message = "Stop distance is very tight and may cause overtrading"

    if p90 > 0.2:
        status = "warning"
        message = "Stop distance is very wide and may reduce trade frequency"

    recommendation = None

    if status == "warning":
        # Recomendación basada en estructura del mercado
        recommended_stop = float(np.clip(p75 := np.quantile(stop_distances, 0.75), 0.002, 0.2))

        recommendation = {
            "suggested_stop_fraction": recommended_stop,
            "explanation": (
                f"Based on market volatility, a stop around "
                f"{recommended_stop:.2%} would better balance trade frequency and risk."
            ),
        }


    return {
        "status": status,
        "metrics": {
            "p50": float(p50),
            "p90": float(p90),
            "p99": float(p99),
        },
        "message": message,
        "recommendation": recommendation,
    }

def _check_sizer_feasibility(
    *,
    df: pd.DataFrame,
    stop: StopConfig,
    risk: RiskConfig,
    account_equity: float,
) -> Dict[str, Any]:

    stop_distances = _compute_stop_distances(df, stop)
    prices = df["close"].values

    risk_amount = account_equity * risk.risk_fraction
    max_position_value = account_equity * risk.max_position_fraction

    effective_risks = []
    effective_positions = []
    ideal_positions = []

    for price, stop_fraction in zip(prices, stop_distances):
        if stop_fraction <= 0 or price <= 0:
            continue

        ideal_position_value = risk_amount / stop_fraction
        ideal_positions.append(ideal_position_value)

        position_value = min(ideal_position_value, max_position_value)
        effective_positions.append(position_value)

        effective_risk = position_value * stop_fraction / account_equity
        effective_risks.append(effective_risk)

    if not effective_risks:
        return {
            "status": "fail",
            "message": "Unable to compute sizing feasibility",
        }

    eff_risk_median = float(np.median(effective_risks))
    ideal_position_median = float(np.median(ideal_positions))
    effective_position_median = float(np.median(effective_positions))

    status = "ok"
    message = "Risk sizing looks feasible"

    if eff_risk_median < risk.risk_fraction * 0.1:
        status = "warning"
        message = (
            "Effective risk is much lower than intended. "
            "Position sizing is heavily capped by limits."
        )

    if eff_risk_median > risk.risk_fraction * 1.5:
        status = "fail"
        message = (
            "Effective risk exceeds intended risk. "
            "Risk configuration is unsafe."
        )

    return {
        "status": status,
        "metrics": {
            "ideal_position_median": ideal_position_median,
            "max_position_value": max_position_value,
            "effective_position_median": effective_position_median,
            "effective_risk_median": eff_risk_median,
        },
        "message": message,
    }

def _compute_position_series(
    *,
    df: pd.DataFrame,
    stop: StopConfig,
    risk: RiskConfig,
    account_equity: float,
) -> Dict[str, Any]:
    """
    Calcula series temporales de sizing, riesgo efectivo
    y distancias de stop (en fracción).
    """

    stop_distances = _compute_stop_distances(df, stop)
    prices = df["close"].values
    timestamps = df.index.astype(str).tolist()

    risk_amount = account_equity * risk.risk_fraction
    max_position_value = account_equity * risk.max_position_fraction

    position_size_pct = []
    effective_risk_pct = []
    stop_distances_out = []

    for price, stop_fraction in zip(prices, stop_distances):
        if stop_fraction is None or stop_fraction <= 0 or price <= 0:
            position_size_pct.append(None)
            effective_risk_pct.append(None)
            stop_distances_out.append(None)
            continue

        position_value = risk_amount / stop_fraction

        if position_value > max_position_value:
            position_value = max_position_value

        pos_pct = position_value / account_equity
        eff_risk = position_value * stop_fraction / account_equity

        position_size_pct.append(float(pos_pct))
        effective_risk_pct.append(float(eff_risk))
        stop_distances_out.append(float(stop_fraction))

    return {
        "timestamps": timestamps,
        "position_size_pct": position_size_pct,
        "effective_risk_pct": effective_risk_pct,
        "stop_distances": stop_distances_out,
    }

# ============================================================
# Utilities
# ============================================================

def _compute_stop_distances(df: pd.DataFrame, stop: StopConfig) -> np.ndarray:
    """
    Devuelve un array con la distancia RELATIVA del stop respecto al precio.
    Es decir: |stop_price - price| / price
    """

    close = df["close"].values.astype(float)

    if stop.type in ("fixed", "trailing"):
        if stop.stop_fraction is None:
            return None

        # stop_fraction ya es relativa (ej. 0.01 = 1%)
        return np.full_like(close, stop.stop_fraction, dtype=float)

    if stop.type == "atr":
        high = df["high"].astype(float)
        low = df["low"].astype(float)
        close_series = df["close"].astype(float)

        tr = pd.concat(
            [
                high - low,
                (high - close_series.shift()).abs(),
                (low - close_series.shift()).abs(),
            ],
            axis=1,
        ).max(axis=1)

        atr = tr.rolling(stop.atr_period).mean()

        # ATR stop distance RELATIVE to price
        stop_distance = (atr * stop.atr_multiplier) / close_series

        return stop_distance.dropna().values.astype(float)

    return None


def _aggregate_status(checks: Dict[str, Any]) -> Literal["ok", "warning", "fail"]:
    """
    Agrega los estados de los checks.
    """

    statuses = [c["status"] for c in checks.values()]

    if "fail" in statuses:
        return "fail"

    if "warning" in statuses:
        return "warning"

    return "ok"


def _build_human_message(status: str, checks: Dict[str, Any]) -> str:
    """
    Construye un mensaje humano de alto nivel.
    """

    if status == "ok":
        return "Risk and stop configuration is compatible with the data"

    if status == "warning":
        return "Risk configuration has warnings. Review details before continuing"

    return "Risk configuration is not usable with the current data"


def _fail_result(message: str) -> Dict[str, Any]:
    """
    Construye una respuesta de fallo inmediato y consistente.
    """
    return {
        "valid": False,
        "status": "fail",
        "checks": {},
        "message": message,
    }
