"""
training_quality_snapshot — assess training data quality and produce a quality gate.

Called by the training autopilot loop (gui_backend.py) to determine:
- readyForTraining: whether training can proceed
- blockedReasons: what's blocking training
- closureCoveragePct: how complete is the trade ledger
- criticalAnomalies: count of serious data issues
- usableTradeCount: how many trades are usable for training
"""

import json
import os
import time
from pathlib import Path
from typing import Any, Dict, Optional

try:
    from src.config_manager import get_app_dir

    _APP_DIR = get_app_dir()
except Exception:
    _APP_DIR = Path(os.environ.get("LOBSTERQUANT_APP_DIR", os.path.expanduser("~/AppData/Roaming/GoldAutoTrade")))

_DATA_DIR = Path(_APP_DIR) / "data"
_OFFLINE_DIR = _DATA_DIR / "offline"
_SNAPSHOT_FILE = _OFFLINE_DIR / "training_quality_snapshot.json"

# in-memory cache (15min TTL = 900s)
_cache: Dict[str, Any] = {}
_cache_ts: float = 0.0
_CACHE_TTL_SEC = 900.0


def _ensure_dir() -> None:
    _SNAPSHOT_FILE.parent.mkdir(parents=True, exist_ok=True)


def _load_ledger_trades() -> list:
    """Load completed trades from ledger snapshots for quality analysis."""
    trades = []
    snapshot_dirs = [
        _DATA_DIR / "bridge" / "archive",
        _DATA_DIR / "bridge" / "state",
    ]
    for sdir in snapshot_dirs:
        try:
            if sdir.is_dir():
                for f in sorted(sdir.glob("*.json"), reverse=True)[:10]:
                    try:
                        data = json.loads(f.read_text(encoding="utf-8"))
                        if isinstance(data, list):
                            trades.extend(data)
                        elif isinstance(data, dict):
                            entries = data.get("trades", data.get("entries", []))
                            if isinstance(entries, list):
                                trades.extend(entries)
                    except (json.JSONDecodeError, OSError):
                        pass
        except PermissionError:
            pass
    return trades


def _load_best_params() -> Optional[Dict[str, Any]]:
    """Load current best_params.json."""
    best_file = _OFFLINE_DIR / "best_params.json"
    try:
        if best_file.exists():
            return json.loads(best_file.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return None


def _compute_quality_metrics(
    trades: list,
    best_params: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """Compute quality metrics from available trade data."""
    total = len(trades)

    if total == 0:
        return {
            "ready_for_training": False,
            "blocked_reasons": ["No completed trades found in ledger"],
            "ledger_fallback": True,
            "closure_coverage_pct": 0.0,
            "critical_anomalies": 0,
            "recovered_closed_ratio_pct": 0.0,
            "natural_closed_ratio_pct": 0.0,
            "usable_trade_count": 0,
            "excluded_incomplete_count": 0,
            "excluded_recovered_count": 0,
            "sample_policy": {},
            "anomalies": [],
        }

    # Classify trades
    closed = [t for t in trades if isinstance(t, dict) and t.get("closed", False)]
    natural_closed = [t for t in closed if not t.get("recovered", False)]
    recovered_closed = [t for t in closed if t.get("recovered", False)]
    incomplete = [t for t in trades if isinstance(t, dict) and not t.get("closed", False)]

    natural_count = len(natural_closed)
    recovered_count = len(recovered_closed)
    incomplete_count = len(incomplete)
    total_closed = natural_count + recovered_count

    closure_coverage_pct = round((total_closed / total) * 100, 1) if total > 0 else 0.0
    recovered_ratio_pct = round((recovered_count / max(total_closed, 1)) * 100, 1)
    natural_ratio_pct = round((natural_count / max(total_closed, 1)) * 100, 1)

    # Detect anomalies
    anomalies = []
    critical_anomalies = 0

    # No closed trades at all
    if total_closed == 0:
        anomalies.append("No closed trades — cannot compute win rate")
        critical_anomalies += 1

    # High recovery ratio (>50%) suggests data quality issues
    if recovered_ratio_pct > 50.0 and total_closed >= 5:
        anomalies.append(f"Recovered-closed ratio {recovered_ratio_pct}% > 50% — ledger may be unreliable")
        critical_anomalies += 1

    # Low closure coverage (<30%)
    if closure_coverage_pct < 30.0 and total >= 10:
        anomalies.append(f"Closure coverage only {closure_coverage_pct}% — training data insufficient")
        critical_anomalies += 1

    # Check best_params age
    if best_params:
        last_updated = best_params.get("last_updated", "")
        if last_updated:
            try:
                # Try ISO format first, then simple date
                from datetime import datetime as dt
                try:
                    updated_ts = dt.strptime(str(last_updated)[:19], "%Y-%m-%dT%H:%M:%S").timestamp()
                except ValueError:
                    updated_ts = dt.strptime(str(last_updated)[:10], "%Y-%m-%d").timestamp()
                age_days = (time.time() - updated_ts) / 86400
                if age_days > 30:
                    anomalies.append(f"Best params are {int(age_days)} days old — may be stale")
            except (ValueError, TypeError):
                pass

    # Determine readiness
    blocked_reasons = []
    ready = True

    if closure_coverage_pct < 20.0:
        blocked_reasons.append(f"Closure coverage too low ({closure_coverage_pct}%)")
        ready = False
    if total_closed < 10:
        blocked_reasons.append(f"Too few closed trades ({total_closed} < 10)")
        ready = False
    if critical_anomalies >= 2:
        blocked_reasons.append(f"Too many critical anomalies ({critical_anomalies})")
        ready = False

    return {
        "ready_for_training": ready,
        "blocked_reasons": blocked_reasons,
        "ledger_fallback": total == 0,
        "closure_coverage_pct": closure_coverage_pct,
        "critical_anomalies": critical_anomalies,
        "recovered_closed_ratio_pct": recovered_ratio_pct,
        "natural_closed_ratio_pct": natural_ratio_pct,
        "natural_closed_transitions": natural_count,
        "recovered_closed_transitions": recovered_count,
        "usable_trade_count": total_closed,
        "excluded_incomplete_count": incomplete_count,
        "excluded_recovered_count": recovered_count,
        "sample_policy": {"min_trades": 10, "min_closure_pct": 20.0, "max_recovered_pct": 50.0},
        "anomalies": anomalies,
    }


def ensure_training_quality_snapshot(
    *,
    force_refresh: bool = False,
    repair_cycle: Optional[Dict[str, Any]] = None,
    source: str = "unknown",
) -> Dict[str, Any]:
    """Generate or refresh the training quality snapshot.

    Args:
        force_refresh: If True, always recompute (ignore cache).
        repair_cycle: Optional repair outcome dict from transition_audit.
        source: Caller identifier for logging.

    Returns:
        Quality snapshot dict with ready_for_training, blocked_reasons, etc.
    """
    global _cache, _cache_ts

    if not force_refresh and _cache and (time.time() - _cache_ts) <= _CACHE_TTL_SEC:
        return _cache

    trades = _load_ledger_trades()
    best_params = _load_best_params()
    metrics = _compute_quality_metrics(trades, best_params)

    # Apply repair cycle insights if available
    if isinstance(repair_cycle, dict) and repair_cycle:
        fixed = repair_cycle.get("fixed_anomalies", 0)
        if fixed > 0:
            metrics["critical_anomalies"] = max(0, metrics["critical_anomalies"] - fixed)
            metrics["blocked_reasons"] = [
                r for r in metrics.get("blocked_reasons", [])
                if "Too many critical anomalies" not in r
            ]
            # Re-check readiness
            if not metrics["blocked_reasons"]:
                metrics["ready_for_training"] = True

    metrics["generated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    metrics["source"] = source
    metrics["total_trades_scanned"] = len(trades)

    # Persist snapshot to disk
    _ensure_dir()
    try:
        _SNAPSHOT_FILE.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding="utf-8")
    except OSError:
        pass

    _cache = metrics
    _cache_ts = time.time()
    return metrics


def read_training_quality_snapshot() -> Dict[str, Any]:
    """Read the most recent quality snapshot from disk (no recompute).

    Returns the cached snapshot if still fresh, otherwise loads from disk.
    """
    global _cache, _cache_ts

    if _cache and (time.time() - _cache_ts) <= _CACHE_TTL_SEC:
        return _cache

    _ensure_dir()
    try:
        if _SNAPSHOT_FILE.exists():
            _cache = json.loads(_SNAPSHOT_FILE.read_text(encoding="utf-8"))
            _cache_ts = time.time()
            return _cache
    except (json.JSONDecodeError, OSError):
        pass
    return {}
