from __future__ import annotations

import calendar
import json
import os
import time
from datetime import datetime
from pathlib import Path
from typing import Any

import config_manager

CONTRACT_SCHEMA_VERSION = "1"
DEFAULT_MACRO_TTL_SEC = 45 * 60  # 45分钟，宏观合约默认有效期


def _contract_path() -> Path:
    return config_manager.get_data_dir() / "agent_decision_contract.json"


def _legacy_interpreter_path() -> Path:
    return config_manager.get_data_dir() / "agent_signal_override.json"


def _legacy_macro_dir() -> Path:
    return config_manager.get_logs_dir() / "agent_decisions"


def _now_iso() -> str:
    return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())


def _read_json_file(path: Path) -> dict[str, Any]:
    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 _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 _write_json_atomic(path, payload):
    from src.atomic_write import atomic_write_json
    return atomic_write_json(path, payload)
def _contract_base() -> dict[str, Any]:
    return {
        "schema_version": CONTRACT_SCHEMA_VERSION,
        "updated_at": _now_iso(),
        "updated_ts": time.time(),
        "macro": {},
        "interpreter": {},
        "decision_policy": {},
        "lineage": {},
    }


def _strategy_market_state_path() -> Path:
    return config_manager.get_config_dir() / "strategy_market_state.json"


def _default_decision_policy() -> dict[str, Any]:
    return {
        "policy_version": "1",
        "profile": "agent_guided_multi_strategy",
        "macro": {
            "role": "directional_gate",
            "degraded_policy": "hold_direction",
            "aligned_scale": 1.1,
            "countertrend_scale": 0.35,
            "neutral_scale": 0.5,
            "direction_loss_guard": {
                "enabled": True,
                "lookback": 8,
                "min_samples": 3,
                "aligned_loss_streak_soft": 2,
                "aligned_loss_streak_hard": 3,
                "counter_win_bias_delta": 0.15,
                "soft_scale": 0.55,
                "hard_scale": 0.2,
                "neutralize_on_hard": False,
            },
        },
        "interpreter": {
            "role": "execution_bias_only",
            "buy_scale": 1.0,
            "sell_scale": 1.0,
            "limit_countertrend_scale": 0.5,
            "block_countertrend_scale": 0.0,
        },
        "risk": {
            "role": "veto_only",
        },
        "execution": {
            "aggregation": "weighted_net_confidence",
            "min_confidence": 0.15,
        },
        "router": {
            "trend": {
                "primary_strategy_id": "ema_macd",
                "support_strategy_ids": ["composite", "elliott_wave"],
                "rank_weights": [0.6, 0.25, 0.15],
            },
            "range": {
                "primary_strategy_id": "wyckoff",
                "support_strategy_ids": ["composite", "chanlun"],
                "rank_weights": [0.6, 0.25, 0.15],
            },
            "uncertain": {
                "primary_strategy_id": "composite",
                "support_strategy_ids": ["ema_macd", "wyckoff"],
                "rank_weights": [0.6, 0.25, 0.15],
            },
        },
    }


def _merge_dict(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
    merged = dict(base)
    for key, value in override.items():
        if isinstance(value, dict) and isinstance(merged.get(key), dict):
            merged[key] = _merge_dict(merged[key], value)
        else:
            merged[key] = value
    return merged


def _load_strategy_market_decision_policy() -> dict[str, Any]:
    state = _read_json_file(_strategy_market_state_path())
    policy = state.get("decision_policy", {}) if isinstance(state.get("decision_policy", {}), dict) else {}
    return policy if policy else {}


def _resolve_decision_policy_seed(raw_policy: dict[str, Any]) -> tuple[dict[str, Any], str]:
    merged = _default_decision_policy()
    if raw_policy:
        merged = _merge_dict(merged, raw_policy)
    market_policy = _load_strategy_market_decision_policy()
    if market_policy:
        return _merge_dict(merged, market_policy), "strategy_market_state"
    if raw_policy:
        return merged, "contract_embedded_policy"
    return merged, "contract_builtin_default"


def _resolve_decision_policy_lineage(raw_policy: dict[str, Any]) -> str:
    _, source = _resolve_decision_policy_seed(raw_policy)
    return source


def _today_macro_path() -> Path:
    today = time.strftime("%Y%m%d")
    return _legacy_macro_dir() / f"direction_{today}.json"


def _normalize_macro_payload(payload: dict[str, Any]) -> dict[str, Any]:
    if not isinstance(payload, dict) or not payload:
        return {}
    direction = str(payload.get("direction", "NEUTRAL") or "NEUTRAL").upper()
    # Alias mapping: accept session_analyzer-style direction labels
    _DIR_ALIASES = {"BUY": "LONG", "BULL": "LONG", "BULLISH": "LONG", "SLIGHTLY_BULLISH": "LONG",
                    "SELL": "SHORT", "BEAR": "SHORT", "BEARISH": "SHORT", "SLIGHTLY_BEARISH": "SHORT",
                    "HOLD": "NEUTRAL", "NONE": "NEUTRAL", "FLAT": "NEUTRAL"}
    direction = _DIR_ALIASES.get(direction, direction)
    if direction not in {"LONG", "SHORT", "NEUTRAL"}:
        direction = "NEUTRAL"
    metadata = payload.get("metadata", {}) if isinstance(payload.get("metadata", {}), dict) else {}
    indicators = payload.get("indicators", {}) if isinstance(payload.get("indicators", {}), dict) else {}
    degraded = bool(payload.get("degraded", False))
    reference_dir = ""
    if metadata:
        reference_dir = str(metadata.get("reference_direction", "") or "")
    if not reference_dir and indicators:
        reference_dir = str(
            indicators.get("previous_macro_direction", "")
            or indicators.get("reference_direction", "")
            or ""
        )
    reference_dir_u = reference_dir.upper() if reference_dir else ""
    if degraded and direction == "NEUTRAL" and reference_dir_u in {"LONG", "SHORT"}:
        direction = reference_dir_u
    try:
        updated_ts = float(payload.get("updated_ts", 0.0) or 0.0)
    except Exception:
        updated_ts = 0.0
    parsed_iso_ts = _parse_updated_iso_to_ts(payload.get("updated"))
    updated_ts = max(updated_ts, parsed_iso_ts)
    if updated_ts <= 0:
        updated_ts = time.time()
    valid_until = float(payload.get("valid_until", updated_ts + DEFAULT_MACRO_TTL_SEC) or (updated_ts + DEFAULT_MACRO_TTL_SEC))
    normalized = {
        "direction": direction,
        "confidence": round(float(payload.get("confidence", payload.get("probability", 0.5)) or 0.5), 4),
        "probability": round(float(payload.get("probability", payload.get("confidence", 0.5)) or 0.5), 4),
        "reason": str(payload.get("reason", "") or ""),
        "phase": str(payload.get("phase", "FRESH") or "FRESH").upper(),
        "degraded": degraded,
        "updated": str(payload.get("updated", "") or _now_iso()),
        "updated_ts": updated_ts,
        "valid_until": valid_until,
        "source": str(payload.get("source", "macro_llm_direction") or "macro_llm_direction"),
        "indicators": indicators,
        "metadata": metadata,
    }
    if isinstance(payload.get("commander_vector", {}), dict) and payload.get("commander_vector", {}):
        normalized["commander_vector"] = payload["commander_vector"]
    quality = payload.get("direction_quality", {})
    if isinstance(quality, dict) and quality:
        normalized["direction_quality"] = {
            "disempowered": bool(quality.get("disempowered", False)),
            "disempower_reason": str(quality.get("disempower_reason", "") or ""),
            "disempower_ts": str(quality.get("disempower_ts", "") or _now_iso()),
            "aligned_loss_streak": int(quality.get("aligned_loss_streak", 0) or 0),
            "aligned_hit_rate": float(quality.get("aligned_hit_rate", 0.0) or 0.0),
            "counter_hit_rate": float(quality.get("counter_hit_rate", 0.0) or 0.0),
            "disempowered_direction": str(quality.get("disempowered_direction", direction) or direction),
        }
    return normalized


def _normalize_interpreter_payload(payload: dict[str, Any]) -> dict[str, Any]:
    if not isinstance(payload, dict) or not payload:
        return {}
    normalized = dict(payload)
    normalized["timestamp"] = float(payload.get("timestamp", time.time()) or time.time())
    normalized["valid_until"] = float(payload.get("valid_until", normalized["timestamp"]) or normalized["timestamp"])
    normalized["source"] = str(payload.get("source", "market_interpreter") or "market_interpreter")
    normalized["execution_advice"] = str(payload.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper()
    normalized["dominant_direction"] = str(payload.get("dominant_direction", "MIXED") or "MIXED").upper()
    normalized["continuation_bias"] = str(payload.get("continuation_bias", "NEUTRAL") or "NEUTRAL").upper()
    normalized["buy_multiplier"] = round(float(payload.get("buy_multiplier", 1.0) or 1.0), 4)
    normalized["sell_multiplier"] = round(float(payload.get("sell_multiplier", 1.0) or 1.0), 4)
    normalized["entry_timing_hint"] = str(payload.get("entry_timing_hint", "WAIT_CLEAR_SIGNAL") or "WAIT_CLEAR_SIGNAL").upper()
    normalized["reason"] = str(payload.get("reason", "") or "")
    return normalized


def _normalize_decision_policy(policy: dict[str, Any]) -> dict[str, Any]:
    payload = policy if isinstance(policy, dict) else {}
    normalized, _ = _resolve_decision_policy_seed(payload)
    return normalized


def _prefer_macro_payload(primary: dict[str, Any], fallback: dict[str, Any]) -> dict[str, Any]:
    if not fallback:
        return primary
    if not primary:
        return fallback
    primary_ts = float(primary.get("updated_ts", 0.0) or 0.0)
    fallback_ts = float(fallback.get("updated_ts", 0.0) or 0.0)
    # Prefer non-degraded over degraded even if slightly older (within 2h window)
    primary_degraded = bool(primary.get("degraded", False))
    fallback_degraded = bool(fallback.get("degraded", False))
    if fallback_degraded and not primary_degraded:
        return primary
    if primary_degraded and not fallback_degraded:
        return fallback
    if fallback_ts > primary_ts + 1e-6:
        return fallback
    if primary_ts > fallback_ts + 1e-6:
        return primary
    if bool(primary.get("degraded", False)) and not bool(fallback.get("degraded", False)):
        return fallback
    return primary


def load_agent_decision_contract(*, include_legacy: bool = True) -> dict[str, Any]:
    payload = _contract_base()
    current = _read_json_file(_contract_path())
    if current:
        payload.update(current)
    raw_decision_policy = payload.get("decision_policy", {}) if isinstance(payload.get("decision_policy", {}), dict) else {}
    payload["schema_version"] = CONTRACT_SCHEMA_VERSION
    payload["macro"] = _normalize_macro_payload(payload.get("macro", {}))
    payload["interpreter"] = _normalize_interpreter_payload(payload.get("interpreter", {}))
    payload["decision_policy"] = _normalize_decision_policy(raw_decision_policy)
    payload["lineage"] = payload.get("lineage", {}) if isinstance(payload.get("lineage", {}), dict) else {}
    if payload["decision_policy"] and not str(payload["lineage"].get("decision_policy", "") or "").strip():
        payload["lineage"]["decision_policy"] = _resolve_decision_policy_lineage(raw_decision_policy)

    if include_legacy:
        legacy_macro = _normalize_macro_payload(_read_json_file(_today_macro_path()))
        preferred_macro = _prefer_macro_payload(payload["macro"], legacy_macro)
        if preferred_macro:
            payload["macro"] = preferred_macro
            if preferred_macro is legacy_macro:
                payload["lineage"]["macro"] = "legacy_direction_registry"
            elif not payload["lineage"].get("macro"):
                payload["lineage"]["macro"] = payload["macro"].get("source", "macro_llm_direction")

    if include_legacy and not payload["interpreter"]:
        legacy_interpreter = _normalize_interpreter_payload(_read_json_file(_legacy_interpreter_path()))
        if legacy_interpreter:
            payload["interpreter"] = legacy_interpreter
            payload["lineage"]["interpreter"] = "legacy_agent_signal_override"

    return payload


def _persist_contract(contract: dict[str, Any]) -> dict[str, Any]:
    normalized = load_agent_decision_contract(include_legacy=False)
    normalized.update(contract if isinstance(contract, dict) else {})
    raw_decision_policy = normalized.get("decision_policy", {}) if isinstance(normalized.get("decision_policy", {}), dict) else {}
    normalized["schema_version"] = CONTRACT_SCHEMA_VERSION
    normalized["updated_at"] = _now_iso()
    normalized["updated_ts"] = time.time()
    normalized["macro"] = _normalize_macro_payload(normalized.get("macro", {}))
    normalized["interpreter"] = _normalize_interpreter_payload(normalized.get("interpreter", {}))
    normalized["decision_policy"] = _normalize_decision_policy(raw_decision_policy)
    normalized["lineage"] = normalized.get("lineage", {}) if isinstance(normalized.get("lineage", {}), dict) else {}
    if normalized["decision_policy"] and not str(normalized["lineage"].get("decision_policy", "") or "").strip():
        normalized["lineage"]["decision_policy"] = _resolve_decision_policy_lineage(raw_decision_policy)
    _write_json_atomic(_contract_path(), normalized)
    return normalized


def update_macro_contract(payload: dict[str, Any], *, source: str = "") -> dict[str, Any]:
    contract = load_agent_decision_contract()
    macro = _normalize_macro_payload(
        {
            **(payload if isinstance(payload, dict) else {}),
            "source": source or (payload.get("source", "") if isinstance(payload, dict) else "") or "macro_llm_direction",
        }
    )
    lineage = (contract.get("lineage", {}) if isinstance(contract.get("lineage", {}), dict) else {}).copy()
    lineage["macro"] = macro.get("source", "macro_llm_direction")
    return _persist_contract({"macro": macro, "lineage": lineage})


def update_interpreter_contract(payload: dict[str, Any], *, source: str = "") -> dict[str, Any]:
    contract = load_agent_decision_contract()
    interpreter = _normalize_interpreter_payload(
        {
            **(payload if isinstance(payload, dict) else {}),
            "source": source or (payload.get("source", "") if isinstance(payload, dict) else "") or "market_interpreter",
        }
    )
    lineage = (contract.get("lineage", {}) if isinstance(contract.get("lineage", {}), dict) else {}).copy()
    lineage["interpreter"] = interpreter.get("source", "market_interpreter")
    return _persist_contract({"interpreter": interpreter, "lineage": lineage})


def update_decision_policy_contract(policy: dict[str, Any], *, source: str = "") -> dict[str, Any]:
    contract = load_agent_decision_contract()
    contract["decision_policy"] = _normalize_decision_policy(policy)
    contract.setdefault("lineage", {})
    contract["lineage"]["decision_policy"] = source or "gui_backend"
    return _persist_contract(contract)


def update_macro_direction_quality(quality: dict[str, Any]) -> dict[str, Any]:
    """由 pipeline 的 direction_loss_guard 调用，写入方向质量数据。

    Commander 下次运行时通过 _read_disempowerment_status() 读取此数据，
    从而感知自己被架空，打破无限循环。

    只传递 macro 字段以避免 pipeline（每 2 秒运行一次）与 GUI 后端之间
    的竞态条件覆盖 decision_policy。
    """
    contract = load_agent_decision_contract()
    macro = contract.get("macro", {}) if isinstance(contract.get("macro", {}), dict) else {}
    existing = macro.get("direction_quality", {}) if isinstance(macro.get("direction_quality", {}), dict) else {}
    merged = {**existing, **quality}
    merged["disempower_ts"] = _now_iso()
    macro["direction_quality"] = merged
    return _persist_contract({"macro": _normalize_macro_payload(macro)})


def repair_decision_contract() -> dict[str, Any]:
    contract = load_agent_decision_contract(include_legacy=True)
    raw_decision_policy = (
        contract.get("decision_policy", {}) if isinstance(contract.get("decision_policy", {}), dict) else {}
    )
    contract["macro"] = _normalize_macro_payload(contract.get("macro", {}))
    contract["interpreter"] = _normalize_interpreter_payload(contract.get("interpreter", {}))
    contract["decision_policy"] = _normalize_decision_policy(raw_decision_policy)
    lineage = contract.get("lineage", {}) if isinstance(contract.get("lineage", {}), dict) else {}
    macro = contract.get("macro", {}) if isinstance(contract.get("macro", {}), dict) else {}
    interpreter = contract.get("interpreter", {}) if isinstance(contract.get("interpreter", {}), dict) else {}
    decision_policy = (
        contract.get("decision_policy", {}) if isinstance(contract.get("decision_policy", {}), dict) else {}
    )
    if macro and not str(lineage.get("macro", "") or "").strip():
        lineage["macro"] = str(macro.get("source", "") or "repaired_contract")
    if interpreter and not str(lineage.get("interpreter", "") or "").strip():
        lineage["interpreter"] = str(interpreter.get("source", "") or "repaired_contract")
    if decision_policy and not str(lineage.get("decision_policy", "") or "").strip():
        lineage["decision_policy"] = _resolve_decision_policy_lineage(raw_decision_policy)
    contract["lineage"] = lineage
    return _persist_contract(contract)


def load_active_interpreter_payload() -> dict[str, Any]:
    payload = _normalize_interpreter_payload(load_agent_decision_contract().get("interpreter", {}))
    if not payload:
        return {}
    if time.time() >= float(payload.get("valid_until", 0.0) or 0.0):
        return {}
    return payload


def load_macro_contract_payload() -> dict[str, Any]:
    contract = load_agent_decision_contract()
    raw_macro = contract.get("macro", {}) if isinstance(contract, dict) else {}
    payload = _normalize_macro_payload(raw_macro)
    if not payload:
        # Fallback: if contract normalization returns empty, try raw macro directly
        if isinstance(raw_macro, dict) and raw_macro.get("direction"):
            payload = dict(raw_macro)
            payload.setdefault("valid_until", time.time() + DEFAULT_MACRO_TTL_SEC)
            payload.setdefault("updated_ts", time.time())
        else:
            return {}
    # TTL check disabled: contract direction should persist until next update
    # (the Commander updates it every ~5 min, so stale data is short-lived)
    return payload
