"""
transition_audit — audit training parameter transitions for anomalies
and run automated repair cycles.

Integrates with the governance brief API (/api/governance_brief) to:
1. Scan the training state for anomalies (stale params, missing files, data gaps)
2. Run a repair cycle that attempts to auto-recover from common issues
3. Write audit reports: transition_health.latest.json, repair_cycle.latest.json,
   anomaly_ledger.latest.jsonl
"""

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

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")))

_AUDIT_DIR = Path(_APP_DIR) / "data" / "learning" / "audit"

# Output files
_HEALTH_FILE = _AUDIT_DIR / "transition_health.latest.json"
_REPAIR_FILE = _AUDIT_DIR / "repair_cycle.latest.json"
_ANOMALY_FILE = _AUDIT_DIR / "anomaly_ledger.latest.jsonl"


def _ensure_audit_dir() -> None:
    _AUDIT_DIR.mkdir(parents=True, exist_ok=True)


def _scan_anomalies() -> List[Dict[str, Any]]:
    """Scan the training subsystem for anomalies.

    Checks:
    - Stale best_params.json (>30 days without update)
    - Missing training_status.json
    - Recovered-closed ratio too high in quality snapshot
    - Zero usable trades
    """
    anomalies = []
    data_dir = Path(_APP_DIR) / "data"
    offline_dir = data_dir / "offline"
    now = time.time()

    # 1. Check best_params.json staleness
    best_file = offline_dir / "best_params.json"
    if best_file.exists():
        try:
            bp = json.loads(best_file.read_text(encoding="utf-8"))
            last_updated = bp.get("last_updated", "")
            if last_updated:
                try:
                    # Parse ISO timestamp
                    from datetime import datetime as dt
                    try:
                        ts = dt.strptime(str(last_updated)[:19], "%Y-%m-%dT%H:%M:%S").timestamp()
                    except ValueError:
                        ts = dt.strptime(str(last_updated)[:10], "%Y-%m-%d").timestamp()
                    age_days = (now - ts) / 86400
                    if age_days > 30:
                        anomalies.append({
                            "id": f"stale_params_{int(ts)}",
                            "type": "stale_params",
                            "severity": "warning",
                            "message": f"Best params are {int(age_days)} days old",
                            "detected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                            "auto_recoverable": False,
                            "recovery_action": "Run offline training to refresh parameters",
                        })
                except (ValueError, TypeError):
                    pass
        except (json.JSONDecodeError, OSError):
            anomalies.append({
                "id": "best_params_corrupt",
                "type": "corrupt_file",
                "severity": "critical",
                "message": "best_params.json is corrupted/unreadable",
                "detected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                "auto_recoverable": False,
                "recovery_action": "Restore from backup or re-run training",
            })
    else:
        anomalies.append({
            "id": "best_params_missing",
            "type": "missing_file",
            "severity": "warning",
            "message": "best_params.json does not exist — no trained model",
            "detected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            "auto_recoverable": True,
            "recovery_action": "Run initial offline training to establish baseline",
        })

    # 2. Check quality snapshot
    quality_file = offline_dir / "training_quality_snapshot.json"
    if quality_file.exists():
        try:
            qs = json.loads(quality_file.read_text(encoding="utf-8"))
            recov_pct = float(qs.get("recovered_closed_ratio_pct", 0))
            usable = int(qs.get("usable_trade_count", 0))

            if recov_pct > 60 and usable >= 5:
                anomalies.append({
                    "id": f"high_recovery_{int(now)}",
                    "type": "data_quality",
                    "severity": "warning",
                    "message": f"Recovered-closed ratio {recov_pct}% > 60% — data reliability suspect",
                    "detected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                    "auto_recoverable": False,
                    "recovery_action": "Manually audit ledger for incorrect close detection",
                })
            if usable == 0:
                anomalies.append({
                    "id": f"zero_usable_{int(now)}",
                    "type": "no_data",
                    "severity": "critical",
                    "message": "Zero usable trades — training cannot proceed",
                    "detected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                    "auto_recoverable": False,
                    "recovery_action": "Ensure MT5 bridge is sending trade data",
                })
        except (json.JSONDecodeError, OSError):
            pass

    # 3. Check training_status.json presence
    status_file = offline_dir / "training_status.json"
    if not status_file.exists():
        anomalies.append({
            "id": "status_missing",
            "type": "missing_file",
            "severity": "info",
            "message": "training_status.json not found — no training has run yet",
            "detected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            "auto_recoverable": True,
            "recovery_action": "Training status will be created on first run",
        })

    return anomalies


def _append_anomaly_ledger(anomalies: List[Dict[str, Any]]) -> None:
    """Append new anomalies to the anomaly ledger (JSONL file)."""
    _ensure_audit_dir()
    try:
        with open(_ANOMALY_FILE, "a", encoding="utf-8") as f:
            for a in anomalies:
                f.write(json.dumps(a, ensure_ascii=False) + "\n")
    except OSError:
        pass


def run_repair_cycle(*, auto_recover: bool = False) -> Dict[str, Any]:
    """Run one repair cycle: scan for anomalies and optionally auto-recover.

    Args:
        auto_recover: If True, attempt automatic fixes for recoverable anomalies.

    Returns:
        Dict with:
            - cycle_id: str
            - anomalies_found: int
            - anomalies: list[dict]
            - auto_recovered: int
            - repair_actions: list[str]
            - health_status: str      (healthy / degraded / critical)
            - generated_at: str
    """
    _ensure_audit_dir()

    cycle_id = f"repair_{int(time.time())}"
    anomalies = _scan_anomalies()
    _append_anomaly_ledger(anomalies)

    auto_recovered = 0
    repair_actions = []

    if auto_recover:
        for a in anomalies:
            if a.get("auto_recoverable"):
                a_type = a.get("type", "")
                if a_type == "missing_file" and "best_params" in a.get("id", ""):
                    # Create minimal best_params.json to unblock
                    best_file = Path(_APP_DIR) / "data" / "offline" / "best_params.json"
                    best_file.parent.mkdir(parents=True, exist_ok=True)
                    if not best_file.exists():
                        default_params = {
                            "strategy": "ema_macd",
                            "version": "auto_init",
                            "last_updated": time.strftime("%Y-%m-%d", time.gmtime()),
                            "_auto_generated": True,
                        }
                        try:
                            best_file.write_text(json.dumps(default_params, ensure_ascii=False, indent=2), encoding="utf-8")
                            auto_recovered += 1
                            repair_actions.append(f"Created minimal best_params.json: {a['message']}")
                        except OSError:
                            pass
                elif a_type == "missing_file" and "status" in a.get("id", ""):
                    # Will be created on next training run — no action needed
                    auto_recovered += 1
                    repair_actions.append(f"Acknowledged missing training_status.json: will be created on first run")

    # Determine health status
    critical_count = sum(1 for a in anomalies if a.get("severity") == "critical")
    warning_count = sum(1 for a in anomalies if a.get("severity") == "warning")

    if critical_count > 0:
        health_status = "critical"
    elif warning_count > 0:
        health_status = "degraded"
    else:
        health_status = "healthy"

    result = {
        "cycle_id": cycle_id,
        "anomalies_found": len(anomalies),
        "anomalies": anomalies,
        "auto_recovered": auto_recovered,
        "auto_recover_enabled": auto_recover,
        "repair_actions": repair_actions,
        "health_status": health_status,
        "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }

    # Persist reports
    try:
        _HEALTH_FILE.write_text(json.dumps({
            "health_status": health_status,
            "anomalies_count": len(anomalies),
            "last_cycle_id": cycle_id,
            "generated_at": result["generated_at"],
        }, ensure_ascii=False, indent=2), encoding="utf-8")
    except OSError:
        pass

    try:
        _REPAIR_FILE.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
    except OSError:
        pass

    return result
