"""
Gold AutoTrade — 统一数据访问层

职责：
  1. 统一所有文件读写路径（单点配置，消除到处硬编码）
  2. 统一价格源访问（消除 Swissquote 重复实现）
  3. 统一 NDJSON 写入（线程安全 + 文件锁）
  4. 统一 OpenClaw 配置解析
  5. 统一 blackboard/runtime 状态文件路径

使用方式：
  from src.data_layer import (
      live_xauusd_price, get_live_bid_ask,
      append_ndjson, read_ndjson, write_json_atomic, read_json,
      blackboard_state_path, runtime_state_path, decision_ledger_path,
      mt5_dir, logs_dir, data_dir,
      FileLock
  )
"""
from __future__ import annotations

import hashlib
import json
import os
import platform
import threading
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

from src.flock_compat import flock, LOCK_EX, LOCK_SH, LOCK_UN, LOCK_NB

# ──────────────────────────────────────────────
# 路径管理
# ──────────────────────────────────────────────

# 缓存 config_manager 结果（延迟加载，支持刷新）
_CONFIG_MANAGER = None
_CONFIG_MANAGER_MT5_DIR = None
_CONFIG_MANAGER_APP_DIR = None
_CONFIG_MANAGER_DATA_DIR = None
_CONFIG_MANAGER_LOGS_DIR = None
_CONFIG_MANAGER_LLM_CONFIG_DIR = None
_CONFIG_CACHE_TTL = 60  # seconds (via config_manager refresh interval)
_CONFIG_CACHE_TS = 0


def _ensure_config_manager():
    """延迟导入 config_manager，避免模块初始化顺序问题。"""
    global _CONFIG_MANAGER, _CONFIG_MANAGER_MT5_DIR, _CONFIG_MANAGER_APP_DIR
    global _CONFIG_MANAGER_DATA_DIR, _CONFIG_MANAGER_LOGS_DIR
    global _CONFIG_MANAGER_LLM_CONFIG_DIR, _CONFIG_CACHE_TS

    now = time.monotonic()
    if _CONFIG_MANAGER is not None and (now - _CONFIG_CACHE_TS) < _CONFIG_CACHE_TTL:
        return

    import config_manager as cm
    _CONFIG_MANAGER_MT5_DIR = cm.get_mt5_dir()
    _CONFIG_MANAGER_APP_DIR = cm.get_app_dir()
    _CONFIG_MANAGER_DATA_DIR = cm.get_data_dir()
    _CONFIG_MANAGER_LOGS_DIR = cm.get_logs_dir()
    _CONFIG_MANAGER_LLM_CONFIG_DIR = cm.get_llm_config_dir()
    _CONFIG_CACHE_TS = now


def refresh_config_cache():
    """强制刷新配置缓存（配置变更后调用）。"""
    global _CONFIG_CACHE_TS
    _CONFIG_CACHE_TS = 0


# ── 项目根目录 ──────────────────────────────

def project_root() -> Path:
    return Path(__file__).resolve().parent.parent


# ── 数据目录 ────────────────────────────────

def mt5_dir() -> Path:
    _ensure_config_manager()
    return _CONFIG_MANAGER_MT5_DIR or (project_root() / "test_mt5_files")


def app_dir() -> Path:
    _ensure_config_manager()
    return _CONFIG_MANAGER_APP_DIR or project_root()


def data_dir() -> Path:
    _ensure_config_manager()
    return _CONFIG_MANAGER_DATA_DIR or (project_root() / "data")


def logs_dir() -> Path:
    _ensure_config_manager()
    return _CONFIG_MANAGER_LOGS_DIR or (project_root() / "logs")


def llm_config_dir() -> Path | None:
    _ensure_config_manager()
    return _CONFIG_MANAGER_LLM_CONFIG_DIR


# ── Bridge 路径 ─────────────────────────────

def bridge_to_ea_dir() -> Path:
    return mt5_dir() / "gold_autotrade" / "bridge" / "to_ea"


def bridge_from_ea_dir() -> Path:
    return mt5_dir() / "gold_autotrade" / "bridge" / "from_ea"


def bridge_archive_dir() -> Path:
    return data_dir() / "bridge" / "archive"


# ── 状态文件路径 ────────────────────────────

def blackboard_state_path() -> Path:
    return data_dir() / "bridge" / "state" / "blackboard_state.json"


def agent_state_path() -> Path:
    return data_dir() / "bridge" / "state" / "agent_state.json"


def agent_state_db_path() -> Path:
    return data_dir() / "bridge" / "state" / "agent_state.db"


def runtime_state_path(tenant_id: str | None = None) -> Path:
    if tenant_id:
        return project_root() / "tenants" / tenant_id / "ops" / "runtime_state.json"
    return project_root() / "ops" / "runtime_state.json"


def execution_state_path() -> Path:
    return data_dir() / "execution_state.json"


# ── 日志文件路径 ────────────────────────────

def runtime_log_path() -> Path:
    return logs_dir() / "runtime.log"


def decision_ledger_path() -> Path:
    return logs_dir() / "decision_ledger.ndjson"


def trading_ledger_path() -> Path:
    return logs_dir() / "trading_ledger.ndjson"


def signal_ledger_path() -> Path:
    return logs_dir() / "signal_ledger.ndjson"


def risk_ledger_path() -> Path:
    return logs_dir() / "risk_events.ndjson"


def gold_trader_log_path(date_str: str | None = None) -> Path:
    if date_str:
        return logs_dir() / f"gold_trader.{date_str}.log"
    return logs_dir() / f"gold_trader.{time.strftime('%Y%m%d')}.log"


def gold_trader_daemon_log_path() -> Path:
    return logs_dir() / "gold_trader.daemon.log"


# ── PID 文件路径 ────────────────────────────

def gold_trader_pid_path() -> Path:
    return logs_dir() / "gold_trader.pid"


# ── OpenClaw 配置路径 ───────────────────────

def openclaw_config_path() -> Path | None:
    """统一的 OpenClaw 配置解析。
    优先级: config_manager → ~/.openclaw/openclaw.json → 项目根目录 openclaw.json
    """
    llm_dir = llm_config_dir()
    if llm_dir:
        candidate = llm_dir / "openclaw.json"
        if candidate.exists():
            return candidate

    candidate = Path.home() / ".openclaw" / "openclaw.json"
    if candidate.exists():
        return candidate

    candidate = project_root() / "openclaw.json"
    if candidate.exists():
        return candidate

    return None


# ──────────────────────────────────────────────
# 文件锁（跨平台）
# ──────────────────────────────────────────────

_LOCK_EX = LOCK_EX
_LOCK_SH = LOCK_SH
_LOCK_NB = LOCK_NB
_LOCK_UN = LOCK_UN


class FileLock:
    """跨进程文件锁上下文管理器。

    用法:
      with FileLock("/path/to/file.lock"):
          # 互斥访问
    """

    def __init__(self, lock_path: str | Path, shared: bool = False, blocking: bool = True, timeout: float = 30.0):
        self._lock_path = Path(lock_path)
        self._shared = shared
        self._blocking = blocking
        self._timeout = timeout
        self._fd: int | None = None
        self._owned = False

    def __enter__(self):
        self._lock_path.parent.mkdir(parents=True, exist_ok=True)
        self._fd = os.open(str(self._lock_path), os.O_CREAT | os.O_RDWR, 0o644)

        flags = _LOCK_SH if self._shared else _LOCK_EX
        if not self._blocking:
            flags |= _LOCK_NB

        deadline = time.monotonic() + self._timeout
        while True:
            try:
                flock(self._fd, flags)
                self._owned = True
                break
            except (IOError, OSError) as e:
                if getattr(e, "errno", None) in (11, 35):  # EAGAIN, EWOULDBLOCK
                    if time.monotonic() >= deadline:
                        raise TimeoutError(f"Could not acquire lock on {self._lock_path} within {self._timeout}s")
                    time.sleep(0.05)
                else:
                    raise

        return self

    def __exit__(self, *args):
        if self._owned and self._fd is not None:
            try:
                flock(self._fd, _LOCK_UN)
            except Exception:
                pass
            try:
                os.close(self._fd)
            except Exception:
                pass
        self._owned = False
        self._fd = None


# ──────────────────────────────────────────────
# NDJSON 日志写入（线程安全 + 进程安全）
# ──────────────────────────────────────────────

_THREAD_LOCAL_BUFFER = threading.local()


def _ndjson_lock_path(path: Path) -> Path:
    return path.with_suffix(path.suffix + ".lock")


def append_ndjson(path: str | Path, record: dict[str, Any]) -> None:
    """线程安全 + 进程安全的 NDJSON 追加写入。

    - 使用文件锁保证跨进程安全
    - 使用 threading.Lock 保证同一进程内安全
    - 自动创建父目录
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    # 获取或创建线程本地锁
    lock_key = str(path)
    tl_lock = getattr(_THREAD_LOCAL_BUFFER, "ndjson_locks", None)
    if tl_lock is None:
        tl_lock = {}
        _THREAD_LOCAL_BUFFER.ndjson_locks = tl_lock

    if lock_key not in tl_lock:
        tl_lock[lock_key] = threading.Lock()

    line = json.dumps(record, ensure_ascii=False, default=str) + "\n"
    with tl_lock[lock_key]:
        try:
            with open(path, "a", encoding="utf-8") as f:
                try:
                    # 先 seek 到开头锁字节 0（msvcrt locking 需要从文件头锁）
                    f.seek(0)
                    flock(f.fileno(), _LOCK_EX)
                    f.seek(0, 2)  # seek end
                    f.write(line)
                    f.flush()
                    flock(f.fileno(), _LOCK_UN)
                except Exception:
                    # 锁不可用时退化到无锁写入
                    f.write(line)
                    f.flush()
        except OSError:
            pass


def read_ndjson(path: str | Path) -> list[dict[str, Any]]:
    """读取 NDJSON 文件，返回所有记录。"""
    path = Path(path)
    if not path.exists():
        return []
    try:
        with open(path, "r", encoding="utf-8") as f:
            try:
                flock(f.fileno(), _LOCK_SH)
                lines = f.readlines()
                flock(f.fileno(), _LOCK_UN)
            except Exception:
                lines = f.readlines()
        results = []
        for line in lines:
            line = line.strip()
            if line:
                try:
                    results.append(json.loads(line))
                except json.JSONDecodeError:
                    pass
        return results
    except OSError:
        return []


# ──────────────────────────────────────────────
# 原子 JSON 写入
# ──────────────────────────────────────────────

def write_json_atomic(path: str | Path, data: Any) -> None:
    """带文件锁的原子 JSON 写入（tmp + replace）。"""
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp_path = path.with_suffix(path.suffix + ".tmp")
    try:
        tmp_path.write_text(json.dumps(data, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
        tmp_path.replace(path)
    except OSError:
        pass


def read_json(path: str | Path) -> Any | None:
    """安全的 JSON 读取，返回 None 如果文件不存在或损坏。"""
    path = Path(path)
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


# ──────────────────────────────────────────────
# 统一价格源
# ──────────────────────────────────────────────

_PRICE_CACHE: dict[str, Any] = {}
_PRICE_CACHE_LOCK = threading.Lock()
_PRICE_CACHE_TTL = 5.0  # seconds (price staleness tolerance)


def live_xauusd_price() -> dict[str, Any]:
    """统一的 XAUUSD 实时价格获取。

    优先级:
      1. 缓存（5 秒 TTL）
      2. Swissquote HTTP API
      3. 返回 error dict

    返回: {"source", "symbol", "bid", "ask", "spread", "spread_pips", "timestamp", "success": bool}
    """
    now = time.time()
    with _PRICE_CACHE_LOCK:
        cached = _PRICE_CACHE.get("xauusd")
        if cached and (now - cached.get("_cached_at", 0)) < _PRICE_CACHE_TTL and cached.get("success"):
            return {k: v for k, v in cached.items() if k != "_cached_at"}

    import urllib.request

    url = "https://forex-data-feed.swissquote.com/public-quotes/bboquotes/instrument/XAU/USD"
    result = {
        "source": "swissquote",
        "symbol": "XAUUSD",
        "bid": 0.0,
        "ask": 0.0,
        "spread": 0.0,
        "spread_pips": 0.0,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "success": False,
    }

    try:
        req = urllib.request.Request(url, headers={
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
            "Accept": "application/json",
        })
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode())

        quotes = data[0]["spreadProfilePrices"][0]
        bid = float(quotes["bid"])
        ask = float(quotes["ask"])
        if bid > 0 and ask > 0:
            result["bid"] = bid
            result["ask"] = ask
            result["spread"] = round(ask - bid, 5)
            # ── 自动检测 pip 大小 (3位黄金/5位外汇/2位商品) ──
            _dec_bid = len(str(bid).split(".")[-1]) if "." in str(bid) else 2
            _dec_ask = len(str(ask).split(".")[-1]) if "." in str(ask) else 2
            _decimals = max(_dec_bid, _dec_ask)
            if _decimals >= 4:
                _pip_size = 0.0001   # 5位外汇
            elif _decimals == 3:
                _pip_size = 0.001    # 3位黄金
            elif _decimals == 2:
                _pip_size = 0.01     # 2位商品
            else:
                _pip_size = 0.001
            result["spread_pips"] = round((ask - bid) / _pip_size, 1)
            result["success"] = True
    except Exception:
        pass

    with _PRICE_CACHE_LOCK:
        cached_entry = result.copy()
        cached_entry["_cached_at"] = time.time()
        _PRICE_CACHE["xauusd"] = cached_entry

    return result


def get_live_bid_ask() -> tuple[Optional[float], Optional[float]]:
    """轻量级接口，返回 (bid, ask) 元组。

    替代 bridge/strategy.py 和 bridge/session_analyzer.py 中的重复实现。
    """
    price = live_xauusd_price()
    if price.get("success"):
        return (price["bid"], price["ask"])
    return (None, None)


# ──────────────────────────────────────────────
# 硬件 ID
# ──────────────────────────────────────────────

def get_hardware_id() -> str:
    """生成稳定的硬件指纹。"""
    parts = [platform.node() or "unknown-host", platform.machine() or "unknown-arch"]
    try:
        mac = uuid.getnode()
        parts.append(str(mac))
    except Exception:
        pass
    raw = "|".join(parts)
    return hashlib.sha256(raw.encode()).hexdigest()[:16]


import uuid