from __future__ import annotations

from src.flock_compat import flock, LOCK_EX, LOCK_SH, LOCK_UN
import json
import os
import time
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

import config_manager
try:
    from src.execution_contract import apply_execution_contract_defaults
    from src.market_context import build_regime_audit_payload, normalize_regime_label
except ModuleNotFoundError:
    from execution_contract import apply_execution_contract_defaults
    from market_context import build_regime_audit_payload, normalize_regime_label


OPEN_CONFIRM_GRACE_SEC = 120
CLOSE_TRANSITION_GRACE_SEC = 60  # closed → flat auto-transition after 60s
CLOSING_STALE_GRACE_SEC = 120   # closing → flat auto-transition when MT5 never confirms close
AGENT_FLAT_RECONCILE_SEC = 30


def _runtime_guard_path(data_dir: Optional[Path] = None) -> Path:
    return _data_dir(data_dir) / "runtime_open_guard.json"


def _read_runtime_guard_unlocked(data_dir: Optional[Path] = None) -> dict[str, Any]:
    path = _runtime_guard_path(data_dir)
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return payload if isinstance(payload, dict) else {}


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


def _data_dir(data_dir: Optional[Path] = None) -> Path:
    resolved = data_dir if data_dir is not None else config_manager.get_data_dir()
    resolved.mkdir(parents=True, exist_ok=True)
    return resolved


def lifecycle_path(data_dir: Optional[Path] = None) -> Path:
    return _data_dir(data_dir) / "position_lifecycle.json"


def _agent_state_path(data_dir: Optional[Path] = None) -> Path:
    return _data_dir(data_dir) / "bridge" / "state" / "agent_state.json"


def _agent_contract_path(data_dir: Optional[Path] = None) -> Path:
    return _data_dir(data_dir) / "agent_decision_contract.json"


def _lock_path(data_dir: Optional[Path] = None) -> Path:
    return _data_dir(data_dir) / "position_lifecycle.lock"


def _lifecycle_path(data_dir: Optional[Path] = None) -> Path:
    return _data_dir(data_dir) / "position_lifecycle.json"


@contextmanager
def _lifecycle_lock(data_dir: Optional[Path] = None, *, shared: bool = False):
    lock_path = _lock_path(data_dir)
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    handle = lock_path.open("a+", encoding="utf-8")
    try:
        handle.seek(0)
        flock(handle.fileno(), LOCK_SH if shared else LOCK_EX)
        yield
    finally:
        try:
            handle.seek(0)
            flock(handle.fileno(), LOCK_UN)
        except Exception:
            pass
        handle.close()


def _default_payload() -> dict[str, Any]:
    now_iso = _now_iso()
    return {
        "updated_at": now_iso,
        "current": {
            "status": "flat",
            "updated_at": now_iso,
        },
        "history": [],
    }


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


def _sanitize_current(current: dict[str, Any], *, now_ts: float | None = None) -> dict[str, Any]:
    if not isinstance(current, dict):
        return _default_payload()["current"]
    normalized = dict(current)
    status = str(normalized.get("status", "flat") or "flat").strip().lower()
    if status not in {"open", "opening", "closing", "closed", "flat"}:
        status = "flat"
    normalized["status"] = status

    if status not in {"open", "opening"}:
        now_value = float(now_ts if now_ts is not None else time.time())
        # Auto-transition closed → flat after grace period so the lifecycle
        # file doesn't permanently show a stale "closed" position.
        if status == "closed":
            closed_at = _parse_iso_ts(normalized.get("closed_at", normalized.get("updated_at", "")))
            age_sec = max(0.0, now_value - closed_at) if closed_at > 0 else 0.0
            if age_sec > CLOSE_TRANSITION_GRACE_SEC:
                normalized.update(
                    {
                        "status": "flat",
                        "status_label": "空闲",
                        "side": "NONE",
                        "ticket": 0,
                        "volume": 0.0,
                        "volume_lots": 0.0,
                        "entry_price": 0.0,
                        "sl": 0.0,
                        "tp": 0.0,
                        "exit_price": 0.0,
                        "realized_pnl": 0.0,
                        "reason": "auto_transition_closed_to_flat",
                    }
                )
        # Keep closing state until MT5 or runtime guard confirms the close.
        if status == "closing":
            closing_at = _parse_iso_ts(normalized.get("updated_at", ""))
            age_sec = max(0.0, now_value - closing_at) if closing_at > 0 else 0.0
            if age_sec > CLOSING_STALE_GRACE_SEC:
                normalized["reason"] = str(normalized.get("reason", "") or "awaiting_close_confirm")
        return normalized

    now_value = float(now_ts if now_ts is not None else time.time())
    updated_ts = max(
        _parse_iso_ts(normalized.get("updated_at")),
        _parse_iso_ts(normalized.get("opened_at")),
    )
    age_sec = max(0.0, now_value - updated_ts) if updated_ts > 0 else OPEN_CONFIRM_GRACE_SEC + 1.0
    ticket = int(normalized.get("ticket", 0) or 0)
    entry_price = float(normalized.get("entry_price", 0.0) or 0.0)

    if status == "open":
        if ticket > 0 and entry_price > 0:
            return normalized
        if age_sec <= OPEN_CONFIRM_GRACE_SEC:
            normalized["status"] = "opening"
            normalized["status_label"] = "开仓中"
            normalized["reason"] = str(normalized.get("reason", "") or "waiting_fill_confirm")
            return normalized

    if ticket <= 0 and age_sec > OPEN_CONFIRM_GRACE_SEC:
        normalized["status"] = "opening"
        normalized["status_label"] = "开仓中"
        normalized["reason"] = str(normalized.get("reason", "") or "waiting_fill_confirm")
    return normalized


def _sanitize_payload(payload: dict[str, Any], *, now_ts: float | None = None) -> dict[str, Any]:
    normalized = dict(payload) if isinstance(payload, dict) else _default_payload()
    current = normalized.get("current", {})
    if not isinstance(current, dict):
        current = _default_payload()["current"]
    normalized["current"] = _sanitize_current(current, now_ts=now_ts)
    if not isinstance(normalized.get("history", []), list):
        normalized["history"] = []
    return normalized


def _read_position_lifecycle_unlocked(data_dir: Optional[Path] = None) -> dict[str, Any]:
    path = lifecycle_path(data_dir)
    payload = _default_payload()
    if not path.exists():
        return payload
    try:
        loaded = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return payload
    if not isinstance(loaded, dict):
        return payload
    payload.update(loaded)
    if not isinstance(payload.get("current", {}), dict):
        payload["current"] = _default_payload()["current"]
    if not isinstance(payload.get("history", []), list):
        payload["history"] = []
    return payload


def _read_agent_state_unlocked(data_dir: Optional[Path] = None) -> dict[str, Any]:
    path = _agent_state_path(data_dir)
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return payload if isinstance(payload, dict) else {}


def _read_agent_contract_unlocked(data_dir: Optional[Path] = None) -> dict[str, Any]:
    path = _agent_contract_path(data_dir)
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return payload if isinstance(payload, dict) else {}


def _safe_float(value: Any, default: float = 0.0) -> float:
    try:
        return float(value or 0.0)
    except (TypeError, ValueError):
        return default


def _safe_int(value: Any, default: int = 0) -> int:
    try:
        return int(value or 0)
    except (TypeError, ValueError):
        return default


def _ts_to_epoch(value: Any) -> float:
    if value in (None, ""):
        return 0.0
    if isinstance(value, (int, float)):
        return float(value)
    text = str(value or "").strip()
    if not text:
        return 0.0
    try:
        return float(text)
    except (TypeError, ValueError):
        return _parse_iso_ts(text)


def _duration_minutes(opened_at: Any, closed_at: Any) -> int:
    opened_ts = _ts_to_epoch(opened_at)
    closed_ts = _ts_to_epoch(closed_at)
    if opened_ts <= 0 or closed_ts <= 0 or closed_ts < opened_ts:
        return 0
    return int(max(0.0, round((closed_ts - opened_ts) / 60.0)))


def _session_label(snapshot: dict[str, Any]) -> str:
    session = snapshot.get("session", {})
    if isinstance(session, dict):
        for key in ("session_bucket", "bucket", "label", "name", "session"):
            value = str(session.get(key, "") or "").strip()
            if value:
                return value.upper()
    value = str(snapshot.get("session_label", snapshot.get("session_bucket", "")) or "").strip()
    return value.upper()


def _resolve_trade_strategy_id(snapshot: dict[str, Any]) -> str:
    for key in (
        "exit_owner_strategy_id",
        "current_manager_strategy_id",
        "entry_owner_strategy_id",
        "attributed_strategy_id",
        "selected_strategy_id",
        "strategy_id",
    ):
        value = str(snapshot.get(key, "") or "").strip()
        if value:
            return value
    return ""


def _aligns_with_agent(side: str, direction: str) -> int:
    side_upper = str(side or "").upper()
    direction_upper = str(direction or "").upper()
    if side_upper == "BUY" and direction_upper == "LONG":
        return 1
    if side_upper == "SELL" and direction_upper == "SHORT":
        return 1
    return 0


def _build_brain_trade_payload(
    closed_snapshot: dict[str, Any],
    agent_state: dict[str, Any],
    agent_contract: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any]]:
    macro = agent_contract.get("macro", {}) if isinstance(agent_contract.get("macro", {}), dict) else {}
    interpreter = (
        closed_snapshot.get("interpreter", {})
        if isinstance(closed_snapshot.get("interpreter", {}), dict)
        else {}
    )
    side = str(closed_snapshot.get("side", "NONE") or "NONE").upper()
    strategy_id = _resolve_trade_strategy_id(closed_snapshot)
    entry_price = _safe_float(closed_snapshot.get("entry_price", closed_snapshot.get("entry", 0.0)), 0.0)
    exit_price = _safe_float(closed_snapshot.get("exit_price", 0.0), 0.0)
    atr_value = _safe_float(
        interpreter.get("atr", interpreter.get("atr_value", agent_state.get("atr_value", 0.0))),
        0.0,
    )
    vol_ratio = _safe_float(
        interpreter.get("volume_ratio", interpreter.get("vol_ratio", agent_state.get("vol_ratio", 1.0))),
        1.0,
    )
    adx_value = _safe_float(
        interpreter.get("adx", interpreter.get("adx_value", agent_state.get("adx", 0.0))),
        0.0,
    )
    agent_direction = str(
        closed_snapshot.get("binding_direction")
        or closed_snapshot.get("agent_direction")
        or macro.get("binding_direction")
        or macro.get("direction")
        or ""
    ).upper()
    exit_reason = str(closed_snapshot.get("reason", closed_snapshot.get("source", "")) or "")
    pnl = _safe_float(closed_snapshot.get("realized_pnl", closed_snapshot.get("pnl", 0.0)), 0.0)
    pnl_atr = 0.0
    if atr_value > 0 and entry_price > 0 and exit_price > 0:
        direction = 1.0 if side == "BUY" else -1.0 if side == "SELL" else 0.0
        pnl_atr = round(((exit_price - entry_price) * direction) / max(atr_value, 1e-9), 4)
    elif pnl != 0.0:
        pnl_atr = round(1.0 if pnl > 0 else -1.0, 4)

    entry_features = {
        "ticket": _safe_int(closed_snapshot.get("ticket", 0), 0),
        "decision_id": str(closed_snapshot.get("decision_id", "") or ""),
        "signal_id": str(closed_snapshot.get("signal_id", "") or ""),
        "opened_at": str(closed_snapshot.get("opened_at", "") or ""),
        "strategy_id": strategy_id,
        "strategy_trust": str(closed_snapshot.get("position_tier", "") or ""),
        "side": side,
        "confidence": _safe_float(closed_snapshot.get("confidence", 0.0), 0.0),
        "entry_price": entry_price,
        "sl_price": _safe_float(closed_snapshot.get("sl", closed_snapshot.get("sl_price", 0.0)), 0.0),
        "tp_price": _safe_float(closed_snapshot.get("tp", closed_snapshot.get("tp_price", 0.0)), 0.0),
        "volume_lots": _safe_float(closed_snapshot.get("volume_lots", closed_snapshot.get("volume", 0.0)), 0.0),
        "session": _session_label(closed_snapshot),
        "regime": str(
            closed_snapshot.get("selection_regime", closed_snapshot.get("regime", "UNKNOWN")) or "UNKNOWN"
        ).upper(),
        "adx_value": adx_value,
        "atr_value": atr_value,
        "vol_ratio": vol_ratio,
        "spread": _safe_float(
            closed_snapshot.get("spread", interpreter.get("spread", agent_state.get("spread_points", 0.0))),
            0.0,
        ),
        "agent_direction": agent_direction,
        "agent_confidence": _safe_float(macro.get("confidence", 0.5), 0.5),
        "agent_aligned": _aligns_with_agent(side, agent_direction),
        "macro_authority": str(
            closed_snapshot.get("macro_authority", macro.get("macro_authority", "")) or ""
        ).upper(),
        "direction_quality": str(macro.get("phase", closed_snapshot.get("direction_quality", "")) or "").upper(),
        "strategy_win_rate_20": _safe_float(
            agent_state.get("strategy_win_rate_20", agent_state.get("win_rate", 0.0)),
            0.0,
        ),
        "strategy_profit_factor": _safe_float(
            agent_state.get("strategy_profit_factor", agent_state.get("profit_factor", 0.0)),
            0.0,
        ),
        "strategy_loss_streak": _safe_int(
            agent_state.get("strategy_loss_streak", agent_state.get("consecutive_losses", 0)),
            0,
        ),
        "risk_level": str(agent_state.get("risk_level", closed_snapshot.get("tier_reason_code", "")) or ""),
        "cooldown_active": 1 if agent_state.get("_shrink_applied") else 0,
        "daily_loss_pct": _safe_float(agent_state.get("daily_loss_pct", 0.0), 0.0),
    }
    outcome = {
        "closed_at": str(closed_snapshot.get("closed_at", "") or ""),
        "pnl": round(pnl, 2),
        "pnl_atr": pnl_atr,
        "is_win": 1 if pnl > 0 else 0,
        "sl_hit": 1 if "SL" in exit_reason.upper() else 0,
        "tp_hit": 1 if "TP" in exit_reason.upper() else 0,
        "duration_min": _duration_minutes(
            closed_snapshot.get("opened_at", ""),
            closed_snapshot.get("closed_at", ""),
        ),
        "exit_reason": exit_reason[:200],
        "peak_profit_atr": 0.0,
        "max_adverse_atr": 0.0,
        "capture_ratio": 0.0,
    }
    return entry_features, outcome


def _record_closed_trade_to_brain(
    closed_snapshot: dict[str, Any],
    *,
    data_dir: Optional[Path] = None,
) -> int | None:
    if not isinstance(closed_snapshot, dict):
        return None
    if not str(closed_snapshot.get("closed_at", "") or "").strip():
        return None
    if not _resolve_trade_strategy_id(closed_snapshot):
        return None
    try:
        from src.learning.trading_brain import get_brain
    except ModuleNotFoundError:
        try:
            from learning.trading_brain import get_brain
        except ModuleNotFoundError:
            return None
    try:
        agent_state = _read_agent_state_unlocked(data_dir)
        agent_contract = _read_agent_contract_unlocked(data_dir)
        entry_features, outcome = _build_brain_trade_payload(
            closed_snapshot,
            agent_state if isinstance(agent_state, dict) else {},
            agent_contract if isinstance(agent_contract, dict) else {},
        )
        return get_brain().record_trade(entry_features, outcome)
    except Exception:
        return None


def _should_flatten_from_agent_state(
    current: dict[str, Any],
    agent_state: dict[str, Any],
    *,
    now_ts: float | None = None,
) -> tuple[bool, str]:
    if not isinstance(current, dict) or not isinstance(agent_state, dict):
        return False, ""
    status = str(current.get("status", "flat") or "flat").strip().lower()
    if status not in {"open", "opening", "closing"}:
        return False, ""
    if bool(agent_state.get("position_open", False)) or int(agent_state.get("position_ticket", 0) or 0) > 0:
        return False, ""

    now_value = float(now_ts if now_ts is not None else time.time())
    current_ts = max(
        _parse_iso_ts(current.get("updated_at")),
        _parse_iso_ts(current.get("opened_at")),
    )
    age_sec = max(0.0, now_value - current_ts) if current_ts > 0 else OPEN_CONFIRM_GRACE_SEC + 1.0
    last_close_ts = max(
        float(agent_state.get("last_close_time", 0.0) or 0.0),
        float(agent_state.get("last_closed_ts", 0.0) or 0.0),
    )

    if last_close_ts > 0 and current_ts > 0 and last_close_ts >= current_ts:
        return True, "agent_state_confirms_flat_after_close"
    if status == "open" and age_sec > AGENT_FLAT_RECONCILE_SEC:
        return True, "agent_state_confirms_flat"
    return False, ""


def _build_payload_from_runtime_guard(
    runtime_guard: dict[str, Any],
    current: dict[str, Any],
) -> dict[str, Any]:
    if not isinstance(runtime_guard, dict) or not runtime_guard:
        return _default_payload()
    normalized_current = dict(current) if isinstance(current, dict) else {}
    position_open = bool(runtime_guard.get("position_open", False))
    if not position_open:
        status = "closed" if normalized_current.get("status") in {"open", "opening", "closing"} else "flat"
        payload_current = {
            "status": status,
            "status_label": "刚平仓" if status == "closed" else "空闲",
            "updated_at": str(runtime_guard.get("updated_at", _now_iso()) or _now_iso()),
            "reason": str(runtime_guard.get("last_close_reason", "runtime_guard_flat") or "runtime_guard_flat"),
            "source": "runtime_open_guard",
        }
        if status == "closed":
            payload_current["closed_at"] = str(runtime_guard.get("updated_at", _now_iso()) or _now_iso())
        return {"updated_at": payload_current["updated_at"], "current": payload_current, "history": []}

    positions_all = runtime_guard.get("positions_all", []) if isinstance(runtime_guard.get("positions_all", []), list) else []
    first = positions_all[0] if positions_all and isinstance(positions_all[0], dict) else {}
    ticket = int(runtime_guard.get("position_ticket", first.get("ticket", 0)) or 0)
    side = str(runtime_guard.get("position_side", first.get("side", "NONE")) or "NONE").upper()
    entry_price = float(runtime_guard.get("position_entry", first.get("entry", first.get("entry_price", 0.0))) or 0.0)
    volume = float(first.get("volume", first.get("volume_lots", 0.0)) or 0.0)
    attribution = runtime_guard.get("position_attribution", {}) if isinstance(runtime_guard.get("position_attribution", {}), dict) else {}
    normalized_attr = normalize_position_attribution(attribution, action="OPEN", fallback=normalized_current)
    payload_current = {
        **normalized_current,
        **normalized_attr,
        "status": "open" if ticket > 0 and entry_price > 0 else "opening",
        "status_label": "持仓中" if ticket > 0 and entry_price > 0 else "开仓中",
        "updated_at": str(runtime_guard.get("updated_at", _now_iso()) or _now_iso()),
        "ticket": ticket,
        "side": side,
        "volume": volume,
        "volume_lots": volume,
        "entry_price": entry_price,
        "sl": float(first.get("sl", 0.0) or 0.0),
        "tp": float(first.get("tp", 0.0) or 0.0),
        "symbol": str(first.get("symbol", normalized_attr.get("symbol", "XAUUSD")) or "XAUUSD"),
        "source": "runtime_open_guard",
    }
    return {"updated_at": payload_current["updated_at"], "current": payload_current, "history": []}


def _should_apply_runtime_guard(
    runtime_guard: dict[str, Any],
    current: dict[str, Any],
) -> bool:
    if not isinstance(runtime_guard, dict) or not runtime_guard:
        return False
    if not isinstance(current, dict) or not current:
        return True

    current_status = str(current.get("status", "flat") or "flat").strip().lower()
    if current_status not in {"open", "opening", "closing"}:
        return True

    positions_all = runtime_guard.get("positions_all", []) if isinstance(runtime_guard.get("positions_all", []), list) else []
    first = positions_all[0] if positions_all and isinstance(positions_all[0], dict) else {}
    guard_open = bool(runtime_guard.get("position_open", False))
    guard_ticket = int(runtime_guard.get("position_ticket", first.get("ticket", 0)) or 0)
    current_ticket = int(current.get("ticket", 0) or 0)

    guard_updated_ts = max(
        _parse_iso_ts(runtime_guard.get("updated_at")),
        float(runtime_guard.get("last_open_time", 0.0) or 0.0),
        float(runtime_guard.get("last_close_time", 0.0) or 0.0),
        float(runtime_guard.get("last_closed_ts", 0.0) or 0.0),
    )
    current_updated_ts = max(
        _parse_iso_ts(current.get("updated_at")),
        _parse_iso_ts(current.get("opened_at")),
        _parse_iso_ts(current.get("closed_at")),
    )
    current_has_attribution = bool(
        str(current.get("entry_owner_strategy_id", "") or "").strip()
        or str(current.get("current_manager_strategy_id", "") or "").strip()
        or str(current.get("entry_owner_instance_id", "") or "").strip()
        or str(current.get("current_manager_instance_id", "") or "").strip()
    )

    if not guard_open:
        return guard_updated_ts >= current_updated_ts
    if current_ticket <= 0:
        return True
    if guard_ticket == current_ticket:
        return True
    if current_has_attribution and current_updated_ts > 0 and guard_updated_ts > 0 and current_updated_ts >= guard_updated_ts:
        return False
    return guard_updated_ts >= current_updated_ts


def _reconcile_payload(payload: dict[str, Any], *, data_dir: Optional[Path] = None, now_ts: float | None = None) -> tuple[dict[str, Any], bool]:
    normalized = _sanitize_payload(payload, now_ts=now_ts)
    changed = normalized != payload
    current = normalized.get("current", {})
    agent_state = _read_agent_state_unlocked(data_dir)
    should_flatten, reason = _should_flatten_from_agent_state(current, agent_state, now_ts=now_ts)
    if not should_flatten:
        return normalized, changed

    if isinstance(current, dict) and current:
        closed_snapshot = _merge_current(
            current,
            {
                "status": "closed",
                "status_label": "刚平仓",
                "closed_at": _now_iso(),
                "reason": reason,
                "source": "agent_state_reconcile",
            },
        )
        _append_history(normalized, closed_snapshot)
    normalized["current"] = {
        "status": "flat",
        "status_label": "空闲",
        "updated_at": _now_iso(),
        "reason": reason,
        "source": "agent_state_reconcile",
    }
    return normalized, True


def load_position_lifecycle(data_dir: Optional[Path] = None) -> dict[str, Any]:
    with _lifecycle_lock(data_dir, shared=True):
        payload = _read_position_lifecycle_unlocked(data_dir)
    current = payload.get("current", {}) if isinstance(payload.get("current", {}), dict) else {}
    runtime_guard = _read_runtime_guard_unlocked(data_dir)
    if _should_apply_runtime_guard(runtime_guard, current):
        payload = _build_payload_from_runtime_guard(runtime_guard, current)
        return _sanitize_payload(payload)
    reconciled, changed = _reconcile_payload(payload, data_dir=data_dir)
    if not changed:
        return reconciled
    with _lifecycle_lock(data_dir):
        latest = _read_position_lifecycle_unlocked(data_dir)
        latest_reconciled, latest_changed = _reconcile_payload(latest, data_dir=data_dir)
        if latest_changed:
            _write_position_lifecycle_unlocked(latest_reconciled, data_dir)
        return latest_reconciled


def reconcile_position_lifecycle(data_dir: Optional[Path] = None, *, now_ts: float | None = None) -> dict[str, Any]:
    with _lifecycle_lock(data_dir):
        payload = _read_position_lifecycle_unlocked(data_dir)
        current = payload.get("current", {}) if isinstance(payload.get("current", {}), dict) else {}
        runtime_guard = _read_runtime_guard_unlocked(data_dir)
        if _should_apply_runtime_guard(runtime_guard, current):
            merged = _build_payload_from_runtime_guard(runtime_guard, current)
            return _sanitize_payload(merged, now_ts=now_ts)
        reconciled, changed = _reconcile_payload(payload, data_dir=data_dir, now_ts=now_ts)
        if changed:
            _write_position_lifecycle_unlocked(reconciled, data_dir)
        return reconciled


def _write_position_lifecycle_unlocked(payload: dict[str, Any], data_dir: Optional[Path] = None) -> Path:
    # Inline path resolution: avoids NameError when old EXE bytecode
    # references _lifecycle_path which may not be in the compiled PYZ.
    _dd = _data_dir(data_dir)
    target = _dd / "position_lifecycle.json"
    path = Path(target)
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp_path = path.with_name(f".{path.name}.{os.getpid()}.{time.time_ns()}.tmp")
    try:
        with open(tmp_path, "w", encoding="utf-8") as handle:
            handle.write(json.dumps(payload, ensure_ascii=False, indent=2))
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(tmp_path, path)
    finally:
        try:
            if tmp_path.exists():
                tmp_path.unlink()
        except Exception:
            pass
    return path


def save_position_lifecycle(payload: dict[str, Any], data_dir: Optional[Path] = None) -> Path:
    with _lifecycle_lock(data_dir):
        return _write_position_lifecycle_unlocked(payload, data_dir)


def _normalize_contributors(raw: Any) -> list[dict[str, Any]]:
    if not isinstance(raw, list):
        return []
    normalized: list[dict[str, Any]] = []
    for item in raw:
        if not isinstance(item, dict):
            continue
        normalized.append(
            {
                "instance_id": str(item.get("instance_id", "") or ""),
                "strategy_id": str(item.get("strategy_id", "") or ""),
                "action": str(item.get("action", "") or ""),
                "side": str(item.get("side", "") or ""),
                "weight": float(item.get("weight", 0.0) or 0.0),
                "confidence": float(item.get("confidence", 0.0) or 0.0),
                "signed_score": float(item.get("signed_score", 0.0) or 0.0),
                "reason": str(item.get("reason", "") or ""),
            }
        )
    return normalized


def normalize_position_attribution(
    attribution: Optional[dict[str, Any]],
    *,
    action: str = "",
    fallback: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    base = fallback if isinstance(fallback, dict) else {}
    payload = attribution if isinstance(attribution, dict) else {}
    merged = {**base, **payload}
    normalized_action = str(action or merged.get("action", "") or "").upper()

    selected_instance_id = str(
        merged.get("selected_instance_id")
        or merged.get("attributed_instance_id")
        or merged.get("instance_id")
        or ""
    ).strip()
    selected_strategy_id = str(
        merged.get("selected_strategy_id")
        or merged.get("attributed_strategy_id")
        or merged.get("strategy_id")
        or ""
    ).strip()

    entry_owner_instance_id = str(
        merged.get("entry_owner_instance_id")
        or base.get("entry_owner_instance_id", "")
        or selected_instance_id
    ).strip()
    entry_owner_strategy_id = str(
        merged.get("entry_owner_strategy_id")
        or base.get("entry_owner_strategy_id", "")
        or selected_strategy_id
    ).strip()

    current_manager_instance_id = str(
        merged.get("current_manager_instance_id")
        or base.get("current_manager_instance_id", "")
        or entry_owner_instance_id
    ).strip()
    current_manager_strategy_id = str(
        merged.get("current_manager_strategy_id")
        or base.get("current_manager_strategy_id", "")
        or entry_owner_strategy_id
    ).strip()

    expected_exit_owner_instance_id = str(
        merged.get("expected_exit_owner_instance_id")
        or base.get("expected_exit_owner_instance_id", "")
        or current_manager_instance_id
        or entry_owner_instance_id
    ).strip()
    expected_exit_owner_strategy_id = str(
        merged.get("expected_exit_owner_strategy_id")
        or base.get("expected_exit_owner_strategy_id", "")
        or current_manager_strategy_id
        or entry_owner_strategy_id
    ).strip()

    exit_owner_instance_id = str(
        merged.get("exit_owner_instance_id")
        or base.get("exit_owner_instance_id", "")
        or (
            selected_instance_id
            if normalized_action in {"CLOSE", "REDUCE"}
            else ""
        )
    ).strip()
    exit_owner_strategy_id = str(
        merged.get("exit_owner_strategy_id")
        or base.get("exit_owner_strategy_id", "")
        or (
            selected_strategy_id
            if normalized_action in {"CLOSE", "REDUCE"}
            else ""
        )
    ).strip()

    if normalized_action == "OPEN":
        current_manager_instance_id = current_manager_instance_id or entry_owner_instance_id
        current_manager_strategy_id = current_manager_strategy_id or entry_owner_strategy_id
        expected_exit_owner_instance_id = expected_exit_owner_instance_id or current_manager_instance_id
        expected_exit_owner_strategy_id = expected_exit_owner_strategy_id or current_manager_strategy_id
    elif normalized_action in {"CLOSE", "REDUCE"}:
        exit_owner_instance_id = exit_owner_instance_id or current_manager_instance_id or entry_owner_instance_id
        exit_owner_strategy_id = exit_owner_strategy_id or current_manager_strategy_id or entry_owner_strategy_id
        current_manager_instance_id = exit_owner_instance_id or current_manager_instance_id
        current_manager_strategy_id = exit_owner_strategy_id or current_manager_strategy_id

    normalized = {
        "decision_id": str(merged.get("decision_id", "") or ""),
        "signal_id": str(merged.get("signal_id", "") or ""),
        "trace_id": str(merged.get("trace_id", "") or ""),
        "strategy_id": entry_owner_strategy_id or str(merged.get("attributed_strategy_id", "") or "").strip() or selected_strategy_id,
        "instance_id": entry_owner_instance_id or str(merged.get("attributed_instance_id", "") or "").strip() or selected_instance_id,
        "selected_instance_id": selected_instance_id,
        "selected_strategy_id": selected_strategy_id,
        "attributed_instance_id": str(merged.get("attributed_instance_id", selected_instance_id) or ""),
        "attributed_strategy_id": str(merged.get("attributed_strategy_id", selected_strategy_id) or ""),
        "entry_owner_instance_id": entry_owner_instance_id,
        "entry_owner_strategy_id": entry_owner_strategy_id,
        "current_manager_instance_id": current_manager_instance_id,
        "current_manager_strategy_id": current_manager_strategy_id,
        "expected_exit_owner_instance_id": expected_exit_owner_instance_id,
        "expected_exit_owner_strategy_id": expected_exit_owner_strategy_id,
        "exit_owner_instance_id": exit_owner_instance_id,
        "exit_owner_strategy_id": exit_owner_strategy_id,
        "regime": normalize_regime_label(merged.get("selection_regime", merged.get("regime", ""))),
        "selection_regime": normalize_regime_label(merged.get("selection_regime", merged.get("regime", ""))),
        "regime_source": str(merged.get("regime_source", base.get("regime_source", "")) or "").upper(),
        "regime_confidence": float(merged.get("regime_confidence", base.get("regime_confidence", 0.0)) or 0.0),
        "route_family": str(merged.get("route_family", base.get("route_family", "")) or "").lower(),
        "agent_direction": str(merged.get("agent_direction", "") or ""),
        "binding_direction": str(
            merged.get("binding_direction", merged.get("agent_direction", "")) or ""
        ).upper(),
        "macro_authority": str(merged.get("macro_authority", "") or "").upper(),
        "allowed_side": str(merged.get("allowed_side", "") or "").upper(),
        "session": merged.get("session", {}) if isinstance(merged.get("session", {}), dict) else {},
        "interpreter": merged.get("interpreter", {}) if isinstance(merged.get("interpreter", {}), dict) else {},
        "confidence": float(merged.get("confidence", 0.0) or 0.0),
        "action": normalized_action or str(merged.get("action", "") or "").upper(),
        "side": str(merged.get("side", "NONE") or "NONE").upper(),
        "symbol": str(merged.get("symbol", "XAUUSD") or "XAUUSD"),
        "volume_lots": float(merged.get("volume_lots", merged.get("volume", 0.0)) or 0.0),
    }
    apply_execution_contract_defaults(normalized, merged)
    normalized.update(build_regime_audit_payload(normalized))
    normalized["contributors"] = _normalize_contributors(merged.get("contributors", []))
    normalized["risk_decision"] = (
        merged.get("risk_decision", {})
        if isinstance(merged.get("risk_decision", {}), dict)
        else {}
    )
    return normalized


def position_state_fields_from_attribution(
    attribution: Optional[dict[str, Any]],
    *,
    action: str = "",
    fallback: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    normalized = normalize_position_attribution(attribution, action=action, fallback=fallback)
    return {
        "position_strategy_id": normalized.get("strategy_id", ""),
        "position_signal_id": normalized.get("signal_id", ""),
        "position_decision_id": normalized.get("decision_id", ""),
        "position_trace_id": normalized.get("trace_id", ""),
        "position_confidence": float(normalized.get("confidence", 0.0) or 0.0),
        "position_regime": normalized.get("regime", "UNKNOWN"),
        "position_selection_regime": normalized.get("selection_regime", normalized.get("regime", "UNKNOWN")),
        "position_regime_source": normalized.get("regime_source", "UNKNOWN"),
        "position_regime_confidence": float(normalized.get("regime_confidence", 0.0) or 0.0),
        "position_route_family": normalized.get("route_family", ""),
        "position_writer_owner": normalized.get("writer_owner", ""),
        "position_execution_owner_mode": normalized.get("execution_owner_mode", ""),
        "position_session_info": normalized.get("session", {}),
        "position_session_metrics": normalized.get("session", {}),
        "position_interpreter": normalized.get("interpreter", {}),
        "position_attribution": normalized,
        "position_agent_direction": normalized.get("agent_direction", ""),
        "position_binding_direction": normalized.get("binding_direction", ""),
        "position_macro_authority": normalized.get("macro_authority", ""),
        "position_allowed_side": normalized.get("allowed_side", ""),
        "position_entry_owner_instance_id": normalized.get("entry_owner_instance_id", ""),
        "position_entry_owner_strategy_id": normalized.get("entry_owner_strategy_id", ""),
        "position_current_manager_instance_id": normalized.get("current_manager_instance_id", ""),
        "position_current_manager_strategy_id": normalized.get("current_manager_strategy_id", ""),
        "position_expected_exit_owner_instance_id": normalized.get("expected_exit_owner_instance_id", ""),
        "position_expected_exit_owner_strategy_id": normalized.get("expected_exit_owner_strategy_id", ""),
        "position_exit_owner_instance_id": normalized.get("exit_owner_instance_id", ""),
        "position_exit_owner_strategy_id": normalized.get("exit_owner_strategy_id", ""),
        "position_contributors": normalized.get("contributors", []),
    }


def _merge_current(current: dict[str, Any], updates: dict[str, Any]) -> dict[str, Any]:
    merged = dict(current) if isinstance(current, dict) else {}
    for key, value in updates.items():
        if value is None:
            continue
        merged[key] = value
    merged["updated_at"] = _now_iso()
    return merged


def _append_history(payload: dict[str, Any], current: dict[str, Any]) -> None:
    history = payload.get("history", [])
    if not isinstance(history, list):
        history = []
    if current:
        history.append(current)
    payload["history"] = history[-50:]


def sync_open_request(
    attribution: Optional[dict[str, Any]],
    *,
    data_dir: Optional[Path] = None,
    reason: str = "",
) -> dict[str, Any]:
    with _lifecycle_lock(data_dir):
        payload = _read_position_lifecycle_unlocked(data_dir)
        normalized = normalize_position_attribution(attribution, action="OPEN")
        payload["current"] = _merge_current(
            payload.get("current", {}),
            {
                **normalized,
                "status": "opening",
                "status_label": "开仓中",
                "reason": str(reason or ""),
            },
        )
        _write_position_lifecycle_unlocked(payload, data_dir)
        return payload


def sync_open_fill(
    attribution: Optional[dict[str, Any]],
    *,
    ticket: int,
    side: str,
    volume: float,
    entry_price: float,
    sl: float = 0.0,
    tp: float = 0.0,
    symbol: str = "XAUUSD",
    data_dir: Optional[Path] = None,
    source: str = "",
) -> dict[str, Any]:
    with _lifecycle_lock(data_dir):
        payload = _read_position_lifecycle_unlocked(data_dir)
        fallback = payload.get("current", {}) if isinstance(payload.get("current", {}), dict) else {}
        normalized = normalize_position_attribution(attribution, action="OPEN", fallback=fallback)
        confirmed_ticket = int(ticket or 0)
        confirmed_entry = float(entry_price or 0.0)
        if confirmed_ticket <= 0 or confirmed_entry <= 0:
            payload["current"] = _merge_current(
                fallback,
                {
                    **normalized,
                    "status": "opening",
                    "status_label": "开仓中",
                    "ticket": 0,
                    "side": str(side or normalized.get("side", "NONE") or "NONE").upper(),
                    "volume": float(volume or normalized.get("volume_lots", 0.0) or 0.0),
                    "entry_price": max(0.0, confirmed_entry),
                    "sl": float(sl or 0.0),
                    "tp": float(tp or 0.0),
                    "symbol": str(symbol or normalized.get("symbol", "XAUUSD") or "XAUUSD"),
                    "reason": "waiting_fill_confirm",
                    "source": str(source or ""),
                },
            )
            _write_position_lifecycle_unlocked(payload, data_dir)
            return payload
        payload["current"] = _merge_current(
            fallback,
            {
                **normalized,
                "status": "open",
                "status_label": "持仓中",
                "ticket": confirmed_ticket,
                "side": str(side or normalized.get("side", "NONE") or "NONE").upper(),
                "volume": float(volume or normalized.get("volume_lots", 0.0) or 0.0),
                "entry_price": confirmed_entry,
                "sl": float(sl or 0.0),
                "tp": float(tp or 0.0),
                "symbol": str(symbol or normalized.get("symbol", "XAUUSD") or "XAUUSD"),
                "opened_at": fallback.get("opened_at") or _now_iso(),
                "source": str(source or ""),
            },
        )
        _write_position_lifecycle_unlocked(payload, data_dir)
        return payload


def sync_close_request(
    attribution: Optional[dict[str, Any]],
    *,
    ticket: int = 0,
    data_dir: Optional[Path] = None,
    reason: str = "",
) -> dict[str, Any]:
    with _lifecycle_lock(data_dir):
        payload = _read_position_lifecycle_unlocked(data_dir)
        fallback = payload.get("current", {}) if isinstance(payload.get("current", {}), dict) else {}
        normalized = normalize_position_attribution(attribution, action="CLOSE", fallback=fallback)
        payload["current"] = _merge_current(
            fallback,
            {
                **normalized,
                "status": "closing",
                "status_label": "平仓中",
                "ticket": int(ticket or fallback.get("ticket", 0) or 0),
                "reason": str(reason or ""),
            },
        )
        _write_position_lifecycle_unlocked(payload, data_dir)
        return payload


def sync_close_fill(
    attribution: Optional[dict[str, Any]],
    *,
    ticket: int = 0,
    exit_price: float = 0.0,
    pnl: float = 0.0,
    side: str = "",
    data_dir: Optional[Path] = None,
    reason: str = "",
    source: str = "",
) -> dict[str, Any]:
    with _lifecycle_lock(data_dir):
        payload = _read_position_lifecycle_unlocked(data_dir)
        fallback = payload.get("current", {}) if isinstance(payload.get("current", {}), dict) else {}
        normalized = normalize_position_attribution(attribution, action="CLOSE", fallback=fallback)
        closed_snapshot = _merge_current(
            fallback,
            {
                **normalized,
                "status": "closed",
                "status_label": "刚平仓",
                "ticket": int(ticket or fallback.get("ticket", 0) or 0),
                "side": str(side or fallback.get("side", normalized.get("side", "NONE")) or "NONE").upper(),
                "exit_price": float(exit_price or 0.0),
                "realized_pnl": float(pnl or 0.0),
                "closed_at": _now_iso(),
                "reason": str(reason or ""),
                "source": str(source or ""),
            },
        )
        brain_trade_id = _record_closed_trade_to_brain(closed_snapshot, data_dir=data_dir)
        closed_snapshot["brain_synced"] = brain_trade_id is not None
        if brain_trade_id is not None:
            closed_snapshot["brain_trade_id"] = int(brain_trade_id)

        # 阈值回溯校准：用真实交易盈亏判断 meta_override 是否正确
        try:
            _agent_state_main = _read_agent_state_unlocked(data_dir)
            _agent_dd = _agent_state_main.get("agent_direction_data", {})
            _meta_override_1 = str(_agent_dd.get("meta_override", "") or "")
            from bridge.commander_client import record_trade_feedback_for_thresholds as _rft
            _rft({
                "side": str(closed_snapshot.get("side", side) or side),
                "pnl": float(closed_snapshot.get("realized_pnl", pnl) or pnl),
                "meta_override": _meta_override_1,
            })
        except Exception:
            pass

        _append_history(payload, closed_snapshot)
        payload["current"] = closed_snapshot
        _write_position_lifecycle_unlocked(payload, data_dir)
        return payload


def sync_flat_recovery(
    *,
    data_dir: Optional[Path] = None,
    reason: str = "",
    source: str = "",
) -> dict[str, Any]:
    with _lifecycle_lock(data_dir):
        payload = _read_position_lifecycle_unlocked(data_dir)
        fallback = payload.get("current", {}) if isinstance(payload.get("current", {}), dict) else {}
        current_status = str(fallback.get("status", "flat") or "flat").strip().lower()
        if current_status not in {"flat", "closed"} and fallback:
            closed_snapshot = _merge_current(
                fallback,
                {
                    "status": "closed",
                    "status_label": "刚平仓",
                    "closed_at": _now_iso(),
                    "reason": str(reason or "bridge_state_confirms_flat"),
                    "source": str(source or "runtime_reconcile"),
                },
            )
            brain_trade_id = _record_closed_trade_to_brain(closed_snapshot, data_dir=data_dir)
            closed_snapshot["brain_synced"] = brain_trade_id is not None
            if brain_trade_id is not None:
                closed_snapshot["brain_trade_id"] = int(brain_trade_id)
            _append_history(payload, closed_snapshot)
        payload["current"] = {
            "status": "flat",
            "status_label": "空闲",
            "updated_at": _now_iso(),
            "reason": str(reason or "bridge_state_confirms_flat"),
            "source": str(source or "runtime_reconcile"),
        }
        _write_position_lifecycle_unlocked(payload, data_dir)
        return payload
