"""Live performance factors backed by true close-only trade outcomes."""

from __future__ import annotations

import json
import math
import time
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, Optional


ARTIFACT_TTL_SEC = 20
LOOKBACK_DAYS = 45
MIN_BUCKET_TRADES = 5


def _artifact_path() -> Path:
    try:
        from config_manager import get_logs_dir

        path = get_logs_dir() / "performance_factors.json"
    except Exception:
        path = Path(__file__).resolve().parents[1] / "logs" / "performance_factors.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    return path


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _to_float(value: Any, default: float = 0.0) -> float:
    try:
        return float(value or 0.0)
    except Exception:
        return float(default)


def _to_int(value: Any, default: int = 0) -> int:
    try:
        return int(value or 0)
    except Exception:
        return int(default)


def _normalize_name(value: Any, fallback: str = "UNKNOWN") -> str:
    text = str(value or "").strip().upper()
    return text or fallback


def _canonical_strategy_id(value: Any) -> str:
    text = str(value or "").strip().lower()
    return text or "unknown"


def _normalize_session_label(value: Any) -> str:
    text = _normalize_name(value)
    normalized = text.replace("-", "_").replace(" ", "_")
    if "ASIA" in normalized and "LONDON" in normalized:
        return "ASIA_LONDON_OVERLAP"
    if "LONDON" in normalized and ("NEW_YORK" in normalized or "NY" in normalized):
        return "LONDON_NY_OVERLAP"
    if normalized in {"NEWYORK", "NEW_YORK", "NEWYORKSESSION"}:
        return "NEW_YORK"
    return normalized


def normalize_regime_label(value: Any) -> str:
    text = _normalize_name(value)
    if text in {"TREND", "TRENDING", "STRONG_TREND", "WEAK_TREND"}:
        return "TRENDING"
    if text in {"RANGE", "RANGING", "LOW_VOLATILITY", "SIDEWAYS"}:
        return "RANGING"
    if text in {"TRANSITION", "UNCERTAIN", "MIXED"}:
        return "TRANSITION"
    return text


def _calc_profit_factor(pnls: list[float]) -> float:
    gross_profit = sum(p for p in pnls if p > 0)
    gross_loss = abs(sum(p for p in pnls if p < 0))
    if gross_loss <= 0:
        return 99.0 if gross_profit > 0 else 0.0
    return round(gross_profit / gross_loss, 4)


def _calc_sharpe(pnls: list[float]) -> float:
    if len(pnls) < 2:
        return 0.0
    mean = sum(pnls) / len(pnls)
    variance = sum((p - mean) ** 2 for p in pnls) / (len(pnls) - 1)
    if variance <= 0:
        return 0.0
    return round(mean / math.sqrt(variance), 4)


def _count_consecutive_losses(pnls: list[float]) -> int:
    count = 0
    for pnl in reversed(pnls):
        if pnl < 0:
            count += 1
            continue
        break
    return count


def _parse_ts(value: Any) -> float:
    text = str(value or "").strip()
    if not text:
        return 0.0
    try:
        return datetime.fromisoformat(text.replace("Z", "+00:00")).timestamp()
    except Exception:
        return 0.0


def _load_close_only_trade_records(since_days: int = LOOKBACK_DAYS) -> list[dict[str, Any]]:
    try:
        from skill_trading_memory import query_trades

        rows = query_trades(limit=None, since_days=since_days)
    except Exception:
        rows = []
    return [row for row in rows if isinstance(row, dict)]


def _record_contains_tokens(record: dict[str, Any], *tokens: str) -> bool:
    haystack = " ".join(
        str(
            record.get(key, "")
        )
        for key in (
            "reason",
            "close_reason",
            "done_reason",
            "source",
            "status_reason",
            "reconcile_reason",
        )
    ).upper()
    return any(token in haystack for token in tokens)


def _normalize_trade_record(record: dict[str, Any]) -> dict[str, Any]:
    indicators = record.get("indicators", {}) if isinstance(record.get("indicators", {}), dict) else {}
    session = _normalize_session_label(
        record.get("session")
        or record.get("session_name")
        or record.get("session_bucket")
        or record.get("session_mode")
    )
    regime = normalize_regime_label(
        record.get("regime")
        or record.get("selection_regime")
        or indicators.get("regime")
        or indicators.get("market_structure")
    )
    strategy_id = _canonical_strategy_id(
        record.get("strategy_id")
        or record.get("selected_strategy_id")
        or record.get("strategy")
    )
    interpreter = _normalize_name(
        record.get("entry_timing_hint")
        or record.get("execution_advice")
        or record.get("interpreter_label")
        or indicators.get("entry_timing_hint")
        or indicators.get("execution_advice")
    )
    entry_style = _normalize_name(
        record.get("entry_style")
        or record.get("order_type")
        or record.get("entry_mode")
        or "MARKET"
    )
    side = _normalize_name(record.get("side"))
    pnl = _to_float(record.get("pnl", record.get("realized_pnl", 0.0)))
    ts_value = record.get("closed_at") or record.get("exit_ts") or record.get("ts")
    entry_key = "|".join((strategy_id, regime, session, entry_style, side))
    recovered = _record_contains_tokens(
        record,
        "GHOST",
        "RECOVER",
        "REPAIR",
        "RECONCILIATION_STATE_ONLY",
    )
    incomplete = strategy_id == "unknown" or session == "UNKNOWN" or regime == "UNKNOWN"
    return {
        **record,
        "_ts": _parse_ts(ts_value),
        "_pnl": pnl,
        "_strategy_id": strategy_id,
        "_session": session,
        "_regime": regime,
        "_interpreter": interpreter,
        "_entry_style": entry_style,
        "_side": side,
        "_entry_key": entry_key,
        "_recovered": recovered,
        "_incomplete": incomplete,
        "_usable": not recovered and not incomplete,
    }


def _bucket_adjustments(
    category: str,
    trades: int,
    win_rate_pct: float,
    profit_factor: float,
    total_pnl: float,
    recent_loss_streak: int,
) -> dict[str, Any]:
    result = {
        "decision": "neutral",
        "block": False,
        "live_multiplier": 1.0,
        "confidence_bias": 0.0,
        "reason": "",
    }
    if trades < MIN_BUCKET_TRADES:
        result["reason"] = f"warmup:{trades}<{MIN_BUCKET_TRADES}"
        return result
    hard_block = (
        profit_factor <= 0.55
        or (
            trades >= max(MIN_BUCKET_TRADES + 1, 6)
            and win_rate_pct < 28.0
            and total_pnl < 0
            and recent_loss_streak >= 3
        )
    )
    if category in {"strategy", "entry"} and hard_block:
        result.update(
            {
                "decision": "block",
                "block": True,
                "live_multiplier": 0.25,
                "confidence_bias": -0.12,
                "reason": (
                    f"pf={profit_factor:.2f}|wr={win_rate_pct:.1f}|"
                    f"loss_streak={recent_loss_streak}|trades={trades}"
                ),
            }
        )
        return result
    if (
        profit_factor <= 0.80
        or recent_loss_streak >= 3
        or (trades >= 8 and win_rate_pct < 40.0 and total_pnl < 0)
    ):
        result.update(
            {
                "decision": "downweight",
                "live_multiplier": 0.55 if category in {"strategy", "entry"} else 0.72,
                "confidence_bias": -0.06 if category in {"strategy", "entry"} else -0.03,
                "reason": (
                    f"pf={profit_factor:.2f}|wr={win_rate_pct:.1f}|"
                    f"loss_streak={recent_loss_streak}"
                ),
            }
        )
        return result
    if profit_factor >= 1.35 and win_rate_pct >= 60.0 and total_pnl > 0:
        result.update(
            {
                "decision": "favor",
                "live_multiplier": 1.06 if category in {"strategy", "entry"} else 1.03,
                "confidence_bias": 0.03 if category in {"strategy", "entry"} else 0.01,
                "reason": f"pf={profit_factor:.2f}|wr={win_rate_pct:.1f}|trades={trades}",
            }
        )
        return result
    result["reason"] = f"stable:{trades}"
    return result


def _build_bucket_group(
    records: Iterable[dict[str, Any]],
    category: str,
    key_builder: Callable[[dict[str, Any]], str],
) -> dict[str, dict[str, Any]]:
    groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for record in records:
        key = str(key_builder(record) or "").strip()
        if not key:
            continue
        groups[key].append(record)
    payload: dict[str, dict[str, Any]] = {}
    for key, rows in groups.items():
        ordered = sorted(rows, key=lambda item: float(item.get("_ts", 0.0) or 0.0))
        pnls = [float(item.get("_pnl", 0.0) or 0.0) for item in ordered]
        trades = len(ordered)
        wins = sum(1 for pnl in pnls if pnl > 0)
        losses = sum(1 for pnl in pnls if pnl < 0)
        breakeven = trades - wins - losses
        total_pnl = round(sum(pnls), 2)
        avg_pnl = round(total_pnl / max(trades, 1), 2)
        profit_factor = _calc_profit_factor(pnls)
        win_rate_pct = round(100.0 * wins / max(trades, 1), 2)
        recent_loss_streak = _count_consecutive_losses(pnls)
        adjustments = _bucket_adjustments(
            category,
            trades,
            win_rate_pct,
            profit_factor,
            total_pnl,
            recent_loss_streak,
        )
        payload[key] = {
            "name": key,
            "trades": trades,
            "sample_size": trades,
            "wins": wins,
            "losses": losses,
            "breakeven": breakeven,
            "win_rate_pct": win_rate_pct,
            "avg_pnl": avg_pnl,
            "total_pnl": total_pnl,
            "profit_factor": profit_factor,
            "recent_loss_streak": recent_loss_streak,
            "sharpe": _calc_sharpe(pnls),
            "usable": trades >= MIN_BUCKET_TRADES,
            **adjustments,
        }
    return payload


def _build_summary(records: list[dict[str, Any]]) -> dict[str, Any]:
    pnls = [float(item.get("_pnl", 0.0) or 0.0) for item in records]
    total = len(pnls)
    wins = sum(1 for pnl in pnls if pnl > 0)
    losses = sum(1 for pnl in pnls if pnl < 0)
    win_pnls = [pnl for pnl in pnls if pnl > 0]
    loss_pnls = [pnl for pnl in pnls if pnl < 0]
    return {
        "total_trades": total,
        "wins": wins,
        "losses": losses,
        "win_rate_30": round(wins / max(total, 1), 4) if total else 0.0,
        "avg_win": round(sum(win_pnls) / max(len(win_pnls), 1), 2) if win_pnls else 0.0,
        "avg_loss": round(abs(sum(loss_pnls)) / max(len(loss_pnls), 1), 2) if loss_pnls else 0.0,
        "profit_factor": _calc_profit_factor(pnls),
        "sharpe_30": _calc_sharpe(pnls),
        "recent_pnl_5": round(sum(pnls[-5:]), 2) if pnls else 0.0,
        "consecutive_losses_now": _count_consecutive_losses(pnls),
        "max_consecutive_losses": max(
            (_count_consecutive_losses(pnls[: idx + 1]) for idx in range(len(pnls))),
            default=0,
        ),
        "total_pnl": round(sum(pnls), 2),
    }


def _collect_pretrade_lists(group: dict[str, dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    blocked: list[dict[str, Any]] = []
    weak: list[dict[str, Any]] = []
    for name, stats in group.items():
        if not isinstance(stats, dict) or not bool(stats.get("usable", False)):
            continue
        row = {
            "name": str(name),
            "trades": int(stats.get("trades", 0) or 0),
            "win_rate_pct": float(stats.get("win_rate_pct", 0.0) or 0.0),
            "profit_factor": float(stats.get("profit_factor", 0.0) or 0.0),
            "live_multiplier": float(stats.get("live_multiplier", 1.0) or 1.0),
            "reason": str(stats.get("reason", "") or ""),
        }
        if bool(stats.get("block", False)):
            blocked.append(row)
        elif float(stats.get("live_multiplier", 1.0) or 1.0) < 1.0:
            weak.append(row)
    blocked.sort(key=lambda item: (item["profit_factor"], item["win_rate_pct"], -item["trades"]))
    weak.sort(key=lambda item: (item["live_multiplier"], item["profit_factor"], item["win_rate_pct"]))
    return blocked[:6], weak[:6]


def _write_json_atomic(path: Path, payload: dict[str, Any]) -> None:
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(payload, ensure_ascii=True, indent=2), encoding="utf-8")
    tmp.replace(path)


def refresh_live_performance_factors(force_refresh: bool = True) -> Dict[str, Any]:
    del force_refresh
    result: Dict[str, Any] = {
        "available": False,
        "error": None,
        "updated_at": _utc_now_iso(),
        "last_updated": None,
        "factors": {},
        "summary": {},
        "quality": {},
        "session": {},
        "regime": {},
        "strategy": {},
        "interpreter": {},
        "entry": {},
        "pretrade": {},
    }
    try:
        normalized = [_normalize_trade_record(row) for row in _load_close_only_trade_records()]
        if not normalized:
            result["error"] = "NO_TRADE_DATA"
            return result
        usable = [row for row in normalized if bool(row.get("_usable", False))]
        excluded_incomplete = sum(1 for row in normalized if bool(row.get("_incomplete", False)))
        excluded_recovered = sum(1 for row in normalized if bool(row.get("_recovered", False)))
        if not usable:
            result["error"] = "NO_USABLE_CLOSE_TRADES"
            result["quality"] = {
                "ready": False,
                "usable_trade_count": 0,
                "min_bucket_trades": MIN_BUCKET_TRADES,
                "excluded_incomplete_count": excluded_incomplete,
                "excluded_recovered_count": excluded_recovered,
            }
            return result
        summary = _build_summary(usable)
        session_group = _build_bucket_group(usable, "session", lambda row: str(row.get("_session", "UNKNOWN")))
        regime_group = _build_bucket_group(usable, "regime", lambda row: str(row.get("_regime", "UNKNOWN")))
        strategy_group = _build_bucket_group(usable, "strategy", lambda row: str(row.get("_strategy_id", "unknown")))
        interpreter_group = _build_bucket_group(usable, "interpreter", lambda row: str(row.get("_interpreter", "UNKNOWN")))
        entry_group = _build_bucket_group(usable, "entry", lambda row: str(row.get("_entry_key", "")))
        blocked_strategies, weak_strategies = _collect_pretrade_lists(strategy_group)
        blocked_entries, weak_entries = _collect_pretrade_lists(entry_group)
        quality = {
            "ready": len(usable) >= MIN_BUCKET_TRADES,
            "usable_trade_count": len(usable),
            "min_bucket_trades": MIN_BUCKET_TRADES,
            "excluded_incomplete_count": excluded_incomplete,
            "excluded_recovered_count": excluded_recovered,
            "blocked_strategy_count": len(blocked_strategies),
            "weak_strategy_count": len(weak_strategies),
            "blocked_entry_count": len(blocked_entries),
            "weak_entry_count": len(weak_entries),
        }
        result.update(
            {
                "available": True,
                "updated_at": _utc_now_iso(),
                "last_updated": _utc_now_iso(),
                "summary": summary,
                "factors": summary,
                "quality": quality,
                "session": session_group,
                "regime": regime_group,
                "strategy": strategy_group,
                "interpreter": interpreter_group,
                "entry": entry_group,
                "pretrade": {
                    "blocked_strategies": blocked_strategies,
                    "weak_strategies": weak_strategies,
                    "blocked_entries": blocked_entries,
                    "weak_entries": weak_entries,
                },
            }
        )
        _write_json_atomic(_artifact_path(), result)
        return result
    except Exception as exc:
        result["error"] = str(exc)
        return result


def load_live_performance_factors(force_refresh: bool = False) -> Dict[str, Any]:
    path = _artifact_path()
    if not force_refresh and path.exists():
        try:
            cached = json.loads(path.read_text(encoding="utf-8"))
            updated_at = _parse_ts(cached.get("updated_at"))
            if updated_at > 0 and (time.time() - updated_at) <= ARTIFACT_TTL_SEC:
                return cached
        except Exception:
            pass
    return refresh_live_performance_factors(force_refresh=True)


def _match_bucket(group: dict[str, Any], key: str) -> dict[str, Any]:
    if not isinstance(group, dict) or not key:
        return {}
    stats = group.get(key, {})
    return stats if isinstance(stats, dict) else {}


def build_pretrade_performance_profile(
    signal: dict[str, Any],
    context: dict[str, Any],
    performance_factors: dict[str, Any],
) -> Dict[str, Any]:
    payload = signal.get("payload", signal) if isinstance(signal, dict) else {}
    if not isinstance(payload, dict):
        payload = {}
    performance_factors = performance_factors if isinstance(performance_factors, dict) else {}
    quality = performance_factors.get("quality", {}) if isinstance(performance_factors.get("quality", {}), dict) else {}
    market_context = context.get("market_context", {}) if isinstance(context.get("market_context", {}), dict) else {}
    session_info = context.get("session_info", {}) if isinstance(context.get("session_info", {}), dict) else {}
    interpreter = context.get("interpreter", {}) if isinstance(context.get("interpreter", {}), dict) else {}
    indicators = payload.get("indicators", {}) if isinstance(payload.get("indicators", {}), dict) else {}
    strategy_id = _canonical_strategy_id(
        payload.get("strategy_id")
        or payload.get("_selected_strategy")
        or context.get("selected_strategy")
        or context.get("selected_strategy_id")
    )
    regime = normalize_regime_label(
        payload.get("regime")
        or payload.get("selection_regime")
        or context.get("selection_regime")
        or market_context.get("regime")
        or indicators.get("regime")
        or indicators.get("market_structure")
    )
    session = _normalize_session_label(
        payload.get("session")
        or payload.get("session_name")
        or session_info.get("session_name")
        or session_info.get("session")
        or interpreter.get("session_name")
        or market_context.get("session_name")
        or market_context.get("session_mode")
    )
    interpreter_label = _normalize_name(
        payload.get("entry_timing_hint")
        or payload.get("execution_advice")
        or interpreter.get("entry_timing_hint")
        or interpreter.get("execution_advice")
    )
    entry_style = _normalize_name(
        payload.get("entry_style")
        or (
            payload.get("materialized_opportunity", {})
            if isinstance(payload.get("materialized_opportunity", {}), dict)
            else {}
        ).get("entry_style")
        or "MARKET"
    )
    side = _normalize_name(payload.get("side"), fallback="NONE")
    entry_key = "|".join((strategy_id, regime, session, entry_style, side))
    matches = {
        "strategy": _match_bucket(performance_factors.get("strategy", {}), strategy_id),
        "regime": _match_bucket(performance_factors.get("regime", {}), regime),
        "session": _match_bucket(performance_factors.get("session", {}), session),
        "interpreter": _match_bucket(performance_factors.get("interpreter", {}), interpreter_label),
        "entry": _match_bucket(performance_factors.get("entry", {}), entry_key),
    }
    live_multiplier = 1.0
    confidence_bias = 0.0
    blocked_by: list[dict[str, Any]] = []
    advisories: list[str] = []
    for category, stats in matches.items():
        if not isinstance(stats, dict) or not bool(stats.get("usable", False)):
            continue
        decision = str(stats.get("decision", "neutral") or "neutral")
        multiplier = float(stats.get("live_multiplier", 1.0) or 1.0)
        if multiplier < 1.0:
            live_multiplier = min(live_multiplier, multiplier)
        elif live_multiplier >= 1.0:
            live_multiplier = max(live_multiplier, multiplier)
        confidence_bias += float(stats.get("confidence_bias", 0.0) or 0.0)
        if decision in {"block", "downweight", "favor"}:
            advisories.append(f"{category}:{decision}:{stats.get('reason', '')}")
        if bool(stats.get("block", False)):
            blocked_by.append(
                {
                    "category": category,
                    "name": str(stats.get("name", "")),
                    "reason": str(stats.get("reason", "") or ""),
                }
            )
    confidence_bias = round(max(-0.15, min(0.08, confidence_bias)), 4)
    live_multiplier = round(max(0.1, min(1.12, live_multiplier)), 4)
    block = bool(blocked_by)
    block_reason = ""
    if blocked_by:
        first = blocked_by[0]
        block_reason = (
            f"{str(first.get('category', 'performance')).upper()}_UNDERPERFORMING:"
            f"{first.get('name', '')}:{first.get('reason', '')}"
        )
    return {
        "ready": bool(quality.get("ready", False)),
        "usable_trade_count": int(quality.get("usable_trade_count", 0) or 0),
        "strategy_id": strategy_id,
        "regime": regime,
        "session": session,
        "interpreter": interpreter_label,
        "entry_style": entry_style,
        "entry_key": entry_key,
        "live_multiplier": live_multiplier,
        "confidence_bias": confidence_bias,
        "block": block,
        "block_reason": block_reason,
        "blocked_by": blocked_by,
        "advisories": advisories,
        "matches": {key: value for key, value in matches.items() if isinstance(value, dict) and value},
    }
