"""direction_registry.py — Direction file read/write operations.

Extracted from gold_trader.py to eliminate circular import coupling
between deepseek_agent.py -> gold_trader.py.

This module is self-contained: no imports from bridge/ or src/.
"""

from __future__ import annotations

import calendar
import json
import sys
import time
import pathlib
from datetime import datetime

# ROOT now resolved via frozen_path (PyInstaller-safe)
from frozen_path import APP_ROOT as ROOT
sys.path.insert(0, str(ROOT))

import config_manager

LOGS_DIR = config_manager.get_logs_dir()
AGENT_DIR = LOGS_DIR / "agent_decisions"


def _parse_updated_iso_to_ts(raw: object) -> float:
    text = str(raw or "").strip()
    if not text:
        return 0.0
    for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%d %H:%M:%S"):
        try:
            dt = datetime.strptime(text, fmt)
            return float(calendar.timegm(dt.timetuple()))
        except ValueError:
            continue
    return 0.0


def _effective_updated_ts(payload: dict, decision_file: pathlib.Path | None = None) -> float:
    """Return the best estimate of when direction was last updated, capped at now."""
    candidates: list[float] = []
    now_ts = time.time()
    try:
        payload_ts = float(payload.get("updated_ts", 0.0) or 0.0)
    except Exception:
        payload_ts = 0.0
    # Cap far-future timestamps (bug/attack) to now to prevent direction from appearing forever fresh
    if 0 < payload_ts <= now_ts + 86400:  # allow up to 1 day clock skew
        candidates.append(payload_ts)
    elif payload_ts > 0:
        candidates.append(now_ts)  # far-future → treat as if updated now
    parsed_iso_ts = _parse_updated_iso_to_ts(payload.get("updated"))
    if 0 < parsed_iso_ts <= now_ts + 86400:
        candidates.append(parsed_iso_ts)
    if not candidates and decision_file is not None:
        try:
            candidates.append(min(float(decision_file.stat().st_mtime), now_ts))
        except Exception:
            pass
    return max(candidates) if candidates else 0.0


def save_agent_direction(
    direction, reason, confidence,
    indicators=None, probability=None, phase=None, degraded=False, metadata=None,
    conviction=None, timeframe=None, risk_appetite=None, key_levels=None, macro_regime=None,
) -> dict:
    """保存 Agent 方向决策到 direction_{today}.json（核心文件写入，不含 side effects）.

    返回写入的 data dict.
    """
    AGENT_DIR.mkdir(parents=True, exist_ok=True)
    today = time.strftime("%Y%m%d")
    decision_file = AGENT_DIR / f"direction_{today}.json"
    normalized_direction = str(direction or "NEUTRAL").upper()
    # Whitelist validation: silently map invalid directions to NEUTRAL
    if normalized_direction not in {"LONG", "SHORT", "NEUTRAL"}:
        normalized_direction = "NEUTRAL"
    normalized_probability = float(probability if probability is not None else confidence or 0.5)
    normalized_probability = max(0.05, min(0.99, normalized_probability))
    if degraded:
        normalized_probability = min(normalized_probability, 0.49)
    # Align confidence with probability so downstream readers see consistent values
    normalized_confidence = normalized_probability

    data = {
        "date": today,
        "direction": normalized_direction,
        "reason": reason,
        "confidence": normalized_confidence,
        "probability": normalized_probability,
        "phase": str(phase or ("DEGRADED" if degraded else "FRESH") or "FRESH").upper(),
        "degraded": bool(degraded),
        "indicators": indicators or {},
        "metadata": metadata if isinstance(metadata, dict) else {},
        "updated": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "updated_ts": time.time(),
        "expires": int(time.time()) + 86400,
    }
    vector = {}
    if conviction is not None:
        vector["conviction"] = conviction
    if timeframe is not None:
        vector["timeframe"] = timeframe
    if risk_appetite is not None:
        vector["risk_appetite"] = risk_appetite
    if key_levels is not None:
        vector["key_levels"] = key_levels
    if macro_regime is not None:
        vector["macro_regime"] = macro_regime
    if vector:
        data["commander_vector"] = vector

    try:
        # Atomic write: temp + rename to prevent corruption on crash
        import os as _os, tempfile as _tf
        _fd, _tmp = _tf.mkstemp(suffix=".json", prefix="direction_", dir=str(decision_file.parent))
        with _os.fdopen(_fd, "w", encoding="utf-8") as _fh:
            json.dump(data, _fh, indent=2, ensure_ascii=False)
        _os.replace(_tmp, str(decision_file))
    except Exception:
        pass

    # ── Sync to agent_state.json so load_decision_snapshot() can read it ──
    try:
        _as_path = config_manager.get_data_dir() / "bridge" / "state" / "agent_state.json"
        if _as_path.exists():
            _as_state = json.loads(_as_path.read_text(encoding="utf-8"))
        else:
            _as_state = {}
        if isinstance(_as_state, dict):
            _as_state["agent_direction"] = {
                "direction": normalized_direction,
                "confidence": float(confidence or 0.5),
                "probability": normalized_probability,
                "reason": str(reason or ""),
                "phase": str(phase or "FRESH").upper(),
                "degraded": bool(degraded),
                "updated": data["updated"],
                "updated_ts": data["updated_ts"],
                "valid_until": data["expires"],
                "source": "macro_llm_direction",
            }
            _as_path.parent.mkdir(parents=True, exist_ok=True)
            import os as _os2, tempfile as _tf2
            try:
                _fd2, _tmp2 = _tf2.mkstemp(suffix=".json", prefix="agent_state_", dir=str(_as_path.parent))
                with _os2.fdopen(_fd2, "w", encoding="utf-8") as _fh2:
                    json.dump(_as_state, _fh2, indent=2, ensure_ascii=False)
                    _fh2.flush()
                    _os2.fsync(_fh2.fileno())
                _os2.replace(_tmp2, str(_as_path))
            except Exception:
                _as_path.write_text(json.dumps(_as_state, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as _e:
        try:
            import logging
            logging.warning(f"[direction_registry] agent_state sync failed: {_e}")
        except Exception:
            pass

    # ── Sync to agent_decision_contract.json so pipeline reads fresh direction ──
    try:
        _adc_path = config_manager.get_data_dir() / "agent_decision_contract.json"
        if _adc_path.exists():
            _adc = json.loads(_adc_path.read_text(encoding="utf-8"))
        else:
            _adc = {}
        if isinstance(_adc, dict):
            _adc.setdefault("macro", {})
            _adc["macro"]["direction"] = normalized_direction
            _adc["macro"]["confidence"] = float(confidence or 0.5)
            _adc["macro"]["source"] = "macro_llm_direction"
            _adc["macro"]["updated"] = data["updated"]
            _adc["macro"]["updated_ts"] = data["updated_ts"]
            _adc["macro"]["phase"] = str(phase or "FRESH").upper()
            _adc["macro"]["degraded"] = bool(degraded)
            import os as _os3, tempfile as _tf3
            try:
                _fd3, _tmp3 = _tf3.mkstemp(suffix=".json", prefix="agent_decision_contract_", dir=str(_adc_path.parent))
                with _os3.fdopen(_fd3, "w", encoding="utf-8") as _fh3:
                    json.dump(_adc, _fh3, indent=2, ensure_ascii=False)
                    _fh3.flush()
                    _os3.fsync(_fh3.fileno())
                _os3.replace(_tmp3, str(_adc_path))
            except Exception:
                _adc_path.write_text(json.dumps(_adc, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception:
        pass

    return data


def _load_yesterday_direction_fallback() -> dict | None:
    """Try loading recent direction files as a cold-start fallback.

    Searches up to 3 days back to handle weekends and holidays.
    Returns a degraded payload or None if no prior file exists at all.
    """
    import datetime as _dt_mod
    for days_back in (1, 2, 3):
        target_date = (_dt_mod.datetime.now() - _dt_mod.timedelta(days=days_back)).strftime("%Y%m%d")
        target_file = AGENT_DIR / f"direction_{target_date}.json"
        if target_file.exists():
            break
    else:
        return None  # No file found in 3-day window
    try:
        y_payload = json.loads(target_file.read_text(encoding="utf-8"))
    except Exception:
        return None
    y_direction = str(y_payload.get("direction", "NEUTRAL") or "NEUTRAL").upper()
    if y_direction not in {"LONG", "SHORT", "NEUTRAL"}:
        return None
    y_confidence = float(y_payload.get("confidence", 0.5) or 0.5)
    # 隔夜方向衰减：置信度打 7 折，上限 0.60
    decayed_conf = round(min(0.60, y_confidence * 0.70), 4)
    return {
        "direction": y_direction,
        "probability": decayed_conf,
        "confidence": y_confidence,
        "reason": f"cold_start_yesterday_fallback:{y_direction}",
        "degraded": True,
        "phase": "DEGRADED",
        "updated_ts": 0.0,
        "macro_age_sec": 99999.0,
        "macro_stale": True,
        "source": "yesterday_fallback",
    }


def load_macro_direction_record() -> tuple[str, dict]:
    """读取 Agent 方向决策（核心文件读取）.

    返回: (direction_str, data_dict)
    """
    AGENT_DIR.mkdir(parents=True, exist_ok=True)
    today = time.strftime("%Y%m%d")
    decision_file = AGENT_DIR / f"direction_{today}.json"

    if not decision_file.exists():
        fallback = _load_yesterday_direction_fallback()
        if fallback:
            direction = str(fallback.get("direction", "NEUTRAL") or "NEUTRAL").upper()
            fallback.pop("direction", None)
            return direction, {
                "engine": "macro_llm_direction",
                "direction": direction,
                **fallback,
            }
        return "NEUTRAL", {
            "engine": "macro_llm_direction",
            "direction": "NEUTRAL",
            "probability": 0.52,  # lenient cold-start, still below normal floor
            "phase": "UNKNOWN",
            "reason": "macro_direction_missing",
            "updated_ts": 0.0,
            "macro_age_sec": 999999.0,
            "macro_stale": True,
            "degraded": True,
            "source": "file",
        }

    try:
        payload = json.loads(decision_file.read_text(encoding="utf-8"))
    except Exception as e:
        fallback = _load_yesterday_direction_fallback()
        if fallback:
            direction = str(fallback.get("direction", "NEUTRAL") or "NEUTRAL").upper()
            fallback.pop("direction", None)
            return direction, {
                "engine": "macro_llm_direction",
                "direction": direction,
                **fallback,
                "reason": f"macro_direction_parse_error_fallback:{e}",
            }
        return "NEUTRAL", {
            "engine": "macro_llm_direction",
            "direction": "NEUTRAL",
            "probability": 0.52,
            "phase": "UNKNOWN",
            "reason": f"macro_direction_parse_error:{e}",
            "updated_ts": 0.0,
            "macro_age_sec": 999999.0,
            "macro_stale": True,
            "degraded": True,
            "source": "file",
        }

    direction = str(payload.get("direction", "NEUTRAL") or "NEUTRAL").upper()
    if direction not in {"LONG", "SHORT", "NEUTRAL"}:
        direction = "NEUTRAL"
    probability = float(payload.get("probability", payload.get("confidence", 0.5)) or 0.5)
    degraded = bool(payload.get("degraded", False))
    updated_ts = _effective_updated_ts(payload, decision_file)
    macro_age_sec = max(0.0, time.time() - updated_ts) if updated_ts > 0 else 999999.0
    stale_sec = float(config_manager.get_effective_param("macro_direction_stale_sec", 7200) or 7200)
    macro_stale = degraded or macro_age_sec > max(900.0, stale_sec)
    phase = str(payload.get("phase", "") or "").upper()
    if not phase:
        phase = "DEGRADED" if degraded else ("LATE" if macro_stale else "FRESH")
    probability = max(0.05, min(0.99, probability))
    if degraded:
        probability = min(probability, 0.49)

    data = {
        "engine": "macro_llm_direction",
        "direction": direction,
        "probability": probability,
        "phase": phase,
        "reason": str(payload.get("reason", "macro_llm") or "macro_llm"),
        "updated": str(payload.get("updated", "") or ""),
        "updated_ts": updated_ts,
        "macro_age_sec": round(macro_age_sec, 1),
        "macro_stale": bool(macro_stale),
        "degraded": degraded,
        "source": "file",
        "indicators": payload.get("indicators", {}) if isinstance(payload.get("indicators", {}), dict) else {},
    }
    # Parse commander_vector sub-dict (Phase 1 vectorization)
    vector = payload.get("commander_vector", {})
    if isinstance(vector, dict) and vector:
        cv = {}
        _conv = vector.get("conviction")
        if _conv is not None:
            try:
                cv["conviction"] = max(0.0, min(1.0, float(_conv)))
            except (TypeError, ValueError):
                pass
        _tf = str(vector.get("timeframe", "")) or None
        if _tf:
            cv["timeframe"] = _tf
        _ra = str(vector.get("risk_appetite", "")) or None
        if _ra:
            cv["risk_appetite"] = _ra
        _mr = str(vector.get("macro_regime", "")) or None
        if _mr:
            cv["macro_regime"] = _mr
        _kl = vector.get("key_levels")
        if isinstance(_kl, dict):
            cv["key_levels"] = {k: [float(v) for v in val] if isinstance(val, list) else val
                                for k, val in _kl.items()}
        if cv:
            data["commander_vector"] = cv

    return direction, data


def get_agent_direction() -> tuple[str, dict]:
    """读取 Agent 当天方向决策.

    返回: (LONG/SHORT/NEUTRAL, data_dict)
    """
    try:
        from src.runtime_state.decision_snapshot import load_decision_snapshot

        snapshot = load_decision_snapshot()
        macro = snapshot.get("macro", {}) if isinstance(snapshot.get("macro", {}), dict) else {}
        contract = snapshot.get("contract", {}) if isinstance(snapshot.get("contract", {}), dict) else {}
        direction = str(
            macro.get("binding_direction", macro.get("direction", "NEUTRAL")) or "NEUTRAL"
        ).upper()
        data = {
            "engine": "macro_llm_direction",
            "direction": direction,
            "binding_direction": direction,
            "effective_direction": str(macro.get("effective_direction", "NEUTRAL") or "NEUTRAL").upper(),
            "allowed_side": str(snapshot.get("execution", {}).get("allowed_side", "BOTH") or "BOTH"),
            "probability": float(macro.get("probability", 0.5) or 0.5),
            "confidence": float(macro.get("confidence", macro.get("probability", 0.5)) or 0.5),
            "phase": str(macro.get("phase", macro.get("state", "UNKNOWN")) or "UNKNOWN").upper(),
            "reason": str(macro.get("reason", "") or ""),
            "updated": str(macro.get("updated", "") or ""),
            "updated_ts": float(macro.get("updated_ts", 0.0) or 0.0),
            "macro_age_sec": float(macro.get("age_seconds", 999999.0) or 999999.0),
            "macro_stale": str(macro.get("state", "UNKNOWN") or "UNKNOWN").upper() in {"STALE", "DEGRADED"},
            "degraded": bool(macro.get("degraded", False)),
            "source": str(macro.get("source", "macro_llm_direction") or "macro_llm_direction"),
            "lineage": contract.get("lineage", {}) if isinstance(contract.get("lineage", {}), dict) else {},
            "indicators": macro.get("indicators", {}) if isinstance(macro.get("indicators", {}), dict) else {},
            "metadata": macro.get("metadata", {}) if isinstance(macro.get("metadata", {}), dict) else {},
        }
        return direction, data
    except Exception:
        return load_macro_direction_record()
