from __future__ import annotations
import os
import sys
import json
import pathlib
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional, Union


def _managed_runtime_storage_root() -> Path | None:
    code_dir = Path(__file__).resolve().parent
    for parent in code_dir.parents:
        candidate = parent / "managed_app" / "releases"
        try:
            code_dir.relative_to(candidate)
        except ValueError:
            continue
        return parent
    return None


def _get_storage_root() -> Path:
    managed_root = _managed_runtime_storage_root()
    if managed_root is not None:
        managed_root.mkdir(parents=True, exist_ok=True)
        return managed_root
    return get_app_dir()


# 获取应用级配置文件的位置 (放在项目根目录下，如果是开发模式)
# 如果是打包模式，它会放在用户数据目录下
def get_app_dir() -> Path:
    """获取程序运行时的根数据目录"""
    # 检查是否是被 PyInstaller 打包后的环境
    if getattr(sys, 'frozen', False):
        # 生产环境：使用系统 APPDATA
        if sys.platform == "win32":
            base_dir = Path(os.getenv("APPDATA", "~")) / "GoldAutoTrade"
        else:
            # macOS 下，为了避免沙盒权限问题，直接把数据存在当前执行文件所在的同级目录下
            base_dir = Path(sys.executable).parent / "GoldAutoTradeData"
            
        # [重要] 首次运行时，如果打包环境内置了 config 和 data，释放它们
        if not base_dir.exists():
            base_dir.mkdir(parents=True, exist_ok=True)
            import shutil
            bundled_config = Path(sys._MEIPASS) / "config"
            if bundled_config.exists():
                shutil.copytree(bundled_config, base_dir / "config", dirs_exist_ok=True)
            
            bundled_data = Path(sys._MEIPASS) / "data"
            if bundled_data.exists():
                shutil.copytree(bundled_data, base_dir / "data", dirs_exist_ok=True)
    else:
        # 开发环境：使用当前项目根目录，保证你的现有开发状态不被破坏
        base_dir = Path(__file__).parent

    base_dir.mkdir(parents=True, exist_ok=True)
    return base_dir

def get_app_config_path() -> Path:
    return _get_storage_root() / "app_config.json"

def load_app_config() -> dict:
    config_path = get_app_config_path()
    if config_path.exists():
        try:
            return json.loads(config_path.read_text(encoding="utf-8"))
        except:
            return {}
    return {}

def save_app_config(data: dict):
    config_path = get_app_config_path()
    config = load_app_config()
    config.update(data)
    config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")

def get_mt5_dir():
    """获取MT5的数据目录，优先读配置，没有则自动猜测"""
    config = load_app_config()
    
    # 1. 优先读取用户在 app_config.json 中的配置
    if config.get("mt5_data_dir"):
        p = Path(config["mt5_data_dir"])
        if p.exists():
            return p
            
    # 2. 动态探测 (绝不使用硬编码的用户名)
    if sys.platform == "darwin":
        import getpass
        # 探测 Wine 容器基础路径
        wine_base = Path.home() / "Library/Application Support/net.metaquotes.wine.metatrader5/drive_c/users"
        if wine_base.exists():
            # 自动遍历 Wine 容器内的用户目录 (可能是 user, 可能是当前系统用户名)
            potential_users = ["user", getpass.getuser()]
            try:
                potential_users.extend([d.name for d in wine_base.iterdir() if d.is_dir()])
            except Exception:
                pass
                
            for u in set(potential_users):
                p = wine_base / u / "AppData/Roaming/MetaQuotes/Terminal/Common/Files"
                if p.exists():
                    save_app_config({"mt5_data_dir": str(p)})
                    return p
                    
    elif sys.platform == "win32":
        # Windows 原生探测
        import os
        appdata = os.environ.get("APPDATA")
        if appdata:
            p = Path(appdata) / "MetaQuotes/Terminal/Common/Files"
            if p.exists():
                save_app_config({"mt5_data_dir": str(p)})
                return p
                
    # 3. 最终回退：项目相对路径 (用于 Portable 模式)
    fallback_p = get_app_dir().parent / "bridge/Files"
    if fallback_p.exists():
        return fallback_p

    # 如果实在找不到，返回一个安全的默认占位符，避免抛出异常导致整个系统无法启动
    return get_app_dir().parent / "bridge/Files"

def get_llm_config_dir() -> Path:
    """获取大模型配置文件所在目录 (openclaw.json)"""
    config = load_app_config()
    
    if "llm_config_dir" in config and config["llm_config_dir"]:
        return Path(config["llm_config_dir"])
        
    # 默认 fallback 到 ~/.openclaw
    default_dir = Path.home() / ".openclaw"
    if default_dir.exists():
        save_app_config({"llm_config_dir": str(default_dir)})
        return default_dir
        
    return default_dir

def get_logs_dir() -> Path:
    d = _get_storage_root() / "logs"
    d.mkdir(exist_ok=True)
    return d

def get_data_dir() -> Path:
    d = _get_storage_root() / "data"
    d.mkdir(exist_ok=True)
    return d

def get_config_dir() -> Path:
    d = _get_storage_root() / "config"
    d.mkdir(exist_ok=True)
    return d


def _load_json_dict(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 _runtime_strategy_rules_path() -> Path:
    runtime_file = get_config_dir() / "strategy_rules_v3.json"
    if runtime_file.exists():
        return runtime_file
    repo_file = Path(__file__).resolve().parent / "config" / "strategy_rules_v3.json"
    if repo_file.exists():
        return repo_file
    return runtime_file


def _lookup_dotted_value(payload: dict[str, Any], dotted_key: str) -> Any:
    if dotted_key in payload:
        return payload[dotted_key]
    cursor: Any = payload
    for part in dotted_key.split("."):
        if not isinstance(cursor, dict) or part not in cursor:
            return None
        cursor = cursor[part]
    return cursor


def _candidate_param_keys(param_name: str) -> list[str]:
    alias_map = {
        "base_lots": ["position_sizing.base_lots"],
        "max_open_lots": ["position_sizing.max_lots", "max_position_size"],
        "max_position_size": ["position_sizing.max_lots"],
        "max_positions": ["risk_governor.max_positions"],
        "max_open_positions": ["risk_governor.max_positions", "max_positions"],
        "min_confidence": ["risk_governor.min_confidence", "min_confidence_threshold"],
        "min_confidence_threshold": ["risk_governor.min_confidence", "min_confidence"],
        "sl_pips": ["risk_governor.sl_pips"],
        "tp_pips": ["risk_governor.tp_pips"],
        "max_consecutive_losses": ["risk_governor.max_consecutive_losses"],
        "spread_kill": ["risk_governor.spread_kill"],
    }
    candidates: list[str] = [param_name]
    for alias in alias_map.get(param_name, []):
        if alias not in candidates:
            candidates.append(alias)
    if param_name.endswith("_mult_trend"):
        legacy_name = param_name.replace("_mult_trend", "")
        if legacy_name not in candidates:
            candidates.append(legacy_name)
    return candidates


def _get_schema_default(param_name: str) -> Any:
    """Read a parameter's default value from strategy_params_schema.py (single source of truth)."""
    try:
        from src.strategy_params_schema import get_schema_defaults_flat as _schema_flat

        return _schema_flat().get(param_name)
    except Exception:
        return None


def _resolve_effective_param(param_name: str, default: Any = None) -> tuple[Any, str]:
    candidate_keys = _candidate_param_keys(param_name)

    gui_data = _load_json_dict(get_config_dir() / "risk_governor_rules.json")
    for key in candidate_keys:
        if key in gui_data:
            return gui_data[key], "gui_config"

    effective_data = _load_json_dict(get_logs_dir() / "effective_params.json")
    suggestions = effective_data.get("suggestions", []) if isinstance(effective_data.get("suggestions", []), list) else []
    for sug in suggestions:
        if not isinstance(sug, dict):
            continue
        if sug.get("param") in candidate_keys:
            return sug.get("applied_value", default), "effective_params"

    strategy_rules = _load_json_dict(_runtime_strategy_rules_path())
    for key in candidate_keys:
        value = _lookup_dotted_value(strategy_rules, key)
        if value is not None:
            return value, "strategy_rules_v3"

    for key in candidate_keys:
        schema_default = _get_schema_default(key)
        if schema_default is not None:
            return schema_default, "schema_default"

    return default, "caller_default"


def get_effective_param(param_name: str, default: Any = None):
    """
    全局统一参数获取入口。
    优先级：
    1. GUI手动设置 (risk_governor_rules.json)
    2. effective_params.json 运行时生效值
    3. strategy_rules_v3.json 运行时值
    4. strategy_params_schema.py 中的 schema 默认值 (_schema_defaults)
    5. 调用者传入的 default 回退
    """
    value, _source = _resolve_effective_param(param_name, default)
    return value


def get_effective_param_with_source(param_name: str, default: Any = None) -> tuple[Any, str]:
    """Return both effective value and the source that supplied it."""
    return _resolve_effective_param(param_name, default)


def get_effective_params_snapshot(param_defaults: Optional[dict[str, Any]] = None) -> dict[str, Any]:
    """Build a compact snapshot of the values that live execution actually reads."""
    tracked_defaults: dict[str, Any] = {
        "risk_level": "balanced",
        "max_position_size": 0.10,
        "max_positions": 2,
        "sl_pips": 40,
        "tp_pips": 80,
        "human_in_the_loop": True,
        "min_confidence_threshold": 0.70,
        "account_total_loss_stop_pct": 30.0,
        "base_lots": 0.05,
        "max_open_lots": 0.10,
    }
    if isinstance(param_defaults, dict):
        tracked_defaults.update(param_defaults)

    snapshot: dict[str, Any] = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "sources": {},
    }
    for param_name, fallback in tracked_defaults.items():
        value, source = _resolve_effective_param(param_name, fallback)
        snapshot[param_name] = value
        snapshot["sources"][param_name] = source
    return snapshot
