from __future__ import annotations

import json
import os
import platform
import shutil
import socket
import zipfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


SENSITIVE_KEY_TOKENS = (
    "token",
    "secret",
    "password",
    "api_key",
    "apikey",
    "license_key",
    "authorization",
)
TEXT_TAIL_BYTES = 256_000
TEXT_TAIL_LINES = 1600
ARCHIVE_FILE_LIMIT = 8
DIAGNOSTIC_BUNDLE_RETENTION = 24


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


def _now_compact() -> str:
    return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")


def _contains_sensitive_token(key: str) -> bool:
    lowered = key.strip().lower()
    return any(token in lowered for token in SENSITIVE_KEY_TOKENS)


def _mask_string(value: str) -> str:
    if not value:
        return value
    if len(value) <= 8:
        return "<redacted>"
    return f"{value[:3]}***{value[-2:]}"


def redact_sensitive_data(value: Any, parent_key: str = "") -> Any:
    if _contains_sensitive_token(parent_key):
        if isinstance(value, str):
            return _mask_string(value)
        if isinstance(value, (int, float)):
            return "<redacted-number>"
        return "<redacted>"
    if isinstance(value, dict):
        return {
            str(key): redact_sensitive_data(child, str(key))
            for key, child in value.items()
        }
    if isinstance(value, list):
        return [redact_sensitive_data(item, parent_key) for item in value]
    return value


def _safe_json_load(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 _write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def _write_text(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def _truncate_text(value: str, limit: int = 4000) -> str:
    text = str(value or "")
    if len(text) <= limit:
        return text
    return f"{text[:limit]}...(truncated)"


def _copy_binary_file(src: Path, dst: Path) -> bool:
    if not src.exists() or not src.is_file():
        return False
    dst.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, dst)
    return True


def _copy_redacted_json(src: Path, dst: Path) -> bool:
    payload = _safe_json_load(src)
    if not payload:
        return False
    _write_json(dst, redact_sensitive_data(payload))
    return True


def _copy_text_tail(src: Path, dst: Path, max_bytes: int = TEXT_TAIL_BYTES) -> bool:
    if not src.exists() or not src.is_file():
        return False
    try:
        raw = src.read_bytes()
    except Exception:
        return False
    tail = raw[-max_bytes:]
    try:
        text = tail.decode("utf-8")
    except UnicodeDecodeError:
        text = tail.decode("utf-8", errors="replace")
    lines = text.splitlines()
    if len(lines) > TEXT_TAIL_LINES:
        lines = lines[-TEXT_TAIL_LINES:]
    _write_text(dst, "\n".join(lines) + ("\n" if lines else ""))
    return True


def _recent_files(directory: Path, pattern: str, limit: int = ARCHIVE_FILE_LIMIT) -> list[Path]:
    if not directory.exists():
        return []
    files = [path for path in directory.glob(pattern) if path.is_file()]
    files.sort(key=lambda item: item.stat().st_mtime, reverse=True)
    return files[:limit]


def _path_key(path: Path) -> str:
    try:
        return str(path.resolve())
    except Exception:
        return str(path)


def _normalize_runtime_root(root: Path) -> Path:
    try:
        candidate = root.resolve()
    except Exception:
        candidate = root
    if candidate.name in {"bridge", "src"}:
        parent = candidate.parent
        if (parent / "logs").exists() or (parent / "config").exists() or (parent / "gui_backend.py").exists():
            return parent
    for sub in ["projects/gold_autotrade"]:
        sub_path = candidate / sub
        if (sub_path / "logs").exists() or (sub_path / "gui_backend.py").exists():
            return sub_path
    return candidate


def _managed_storage_root_for_runtime_root(root: Path) -> Path | None:
    normalized_root = _normalize_runtime_root(root)
    windows_appdata = os.environ.get("APPDATA", "").strip()
    if windows_appdata:
        try:
            exe_path = normalized_root / "GoldAutoTradeEngine.exe"
            appdata_root = Path(windows_appdata) / "GoldAutoTrade"
            if exe_path.exists() and (appdata_root / "logs").exists():
                return appdata_root
        except Exception:
            pass
    for parent in normalized_root.parents:
        releases_dir = parent / "managed_app" / "releases"
        try:
            normalized_root.relative_to(releases_dir)
            return parent
        except ValueError:
            continue
    return None


def _iter_active_runtime_roots() -> list[Path]:
    try:
        import psutil
    except Exception:
        return []

    roots: list[Path] = []
    seen: set[str] = set()
    try:
        for proc in psutil.process_iter(["cmdline", "name", "exe"]):
            try:
                cmdline = [str(part) for part in (proc.info.get("cmdline") or []) if str(part).strip()]
            except Exception:
                cmdline = []
            proc_name = str(proc.info.get("name") or "").strip().lower()
            exe_path_raw = str(proc.info.get("exe") or "").strip()
            exe_name = Path(exe_path_raw).name.lower() if exe_path_raw else ""
            combined = " ".join(cmdline)
            is_python_runtime = bool(cmdline) and (
                "gold_trader.py" in combined or "src/pipeline.py" in combined
            )
            is_windows_engine = proc_name == "goldautotradeengine.exe" or exe_name == "goldautotradeengine.exe"
            if not is_python_runtime and not is_windows_engine:
                continue

            proc_roots: list[Path] = []
            try:
                cwd = proc.cwd()
                if cwd:
                    proc_roots.append(Path(cwd))
            except Exception:
                pass

            if is_windows_engine and exe_path_raw:
                try:
                    proc_roots.append(Path(exe_path_raw).parent)
                except Exception:
                    pass

            for part in cmdline:
                try:
                    script_path = Path(part)
                except Exception:
                    continue
                if script_path.name == "gold_trader.py" and script_path.parent.name == "bridge":
                    proc_roots.append(script_path.parent.parent)
                elif script_path.name == "pipeline.py" and script_path.parent.name == "src":
                    proc_roots.append(script_path.parent.parent)

            for root in proc_roots:
                normalized_root = _normalize_runtime_root(root)
                key = _path_key(normalized_root)
                if not key or key in seen:
                    continue
                seen.add(key)
                roots.append(normalized_root)
    except Exception:
        return []
    return roots


def _collect_runtime_roots(app_dir: Path, data_dir: Path, logs_dir: Path, config_dir: Path) -> list[Path]:
    roots: list[Path] = []
    seen: set[str] = set()

    def _register(root: Path | None) -> None:
        if root is None:
            return
        normalized_root = _normalize_runtime_root(root)
        for candidate in (normalized_root, _managed_storage_root_for_runtime_root(normalized_root)):
            if candidate is None:
                continue
            key = _path_key(candidate)
            if not key or key in seen:
                continue
            seen.add(key)
            roots.append(candidate)

    for root in (
        app_dir,
        data_dir.parent,
        logs_dir.parent,
        config_dir.parent,
        *_iter_active_runtime_roots(),
    ):
        _register(root)
    return roots


def _freshest_existing_file(paths: list[Path]) -> Path | None:
    existing: list[Path] = []
    seen: set[str] = set()
    for path in paths:
        key = _path_key(path)
        if key in seen:
            continue
        seen.add(key)
        try:
            if path.exists() and path.is_file():
                existing.append(path)
        except Exception:
            continue
    if not existing:
        return None
    existing.sort(key=lambda item: item.stat().st_mtime, reverse=True)
    return existing[0]


def _runtime_scope_candidates(roots: list[Path], scope: str, *relative_parts: str) -> list[Path]:
    rel_path = Path(*relative_parts)
    return [root / scope / rel_path for root in roots]


def _runtime_root_candidates(roots: list[Path], *relative_parts: str) -> list[Path]:
    rel_path = Path(*relative_parts)
    return [root / rel_path for root in roots]


def _runtime_archive_dirs(roots: list[Path]) -> list[Path]:
    candidates = [root / "data" / "bridge" / "archive" for root in roots]
    deduped: list[Path] = []
    seen: set[str] = set()
    for path in candidates:
        key = _path_key(path)
        if key in seen:
            continue
        seen.add(key)
        deduped.append(path)
    return deduped


def _recent_files_across_dirs(directories: list[Path], pattern: str, limit: int = ARCHIVE_FILE_LIMIT) -> list[Path]:
    files: list[Path] = []
    seen: set[str] = set()
    for directory in directories:
        for path in _recent_files(directory, pattern, limit=limit):
            key = _path_key(path)
            if key in seen:
                continue
            seen.add(key)
            files.append(path)
    files.sort(key=lambda item: item.stat().st_mtime, reverse=True)
    return files[:limit]


def _zip_directory(source_dir: Path, archive_path: Path) -> None:
    archive_path.parent.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as bundle:
        for file_path in sorted(source_dir.rglob("*")):
            if not file_path.is_file():
                continue
            bundle.write(file_path, arcname=str(file_path.relative_to(source_dir)))


def _diagnostics_dir(data_dir: Path) -> Path:
    target = data_dir / "diagnostics"
    target.mkdir(parents=True, exist_ok=True)
    return target


def _prune_diagnostic_bundles(diagnostics_dir: Path, keep_latest: int = DIAGNOSTIC_BUNDLE_RETENTION) -> None:
    if keep_latest <= 0 or not diagnostics_dir.exists():
        return
    bundle_markers = [path for path in diagnostics_dir.glob("diagnostics_*.bundle.json") if path.is_file()]
    if len(bundle_markers) <= keep_latest:
        return
    bundle_markers.sort(key=lambda item: item.stat().st_mtime, reverse=True)
    keep_ids = {
        marker.name.removesuffix(".bundle.json")
        for marker in bundle_markers[:keep_latest]
    }
    prune_ids = [
        marker.name.removesuffix(".bundle.json")
        for marker in bundle_markers[keep_latest:]
    ]
    for bundle_id in prune_ids:
        if bundle_id in keep_ids:
            continue
        for target in diagnostics_dir.glob(f"{bundle_id}*"):
            try:
                if target.is_dir():
                    shutil.rmtree(target, ignore_errors=True)
                else:
                    target.unlink(missing_ok=True)
            except Exception:
                continue


def _load_health_snapshot() -> dict[str, Any]:
    try:
        from src.health.dashboard import get_full_health

        payload = get_full_health(include_disk=True)
        return payload if isinstance(payload, dict) else {}
    except Exception as exc:
        return {"ok": False, "error": f"health_snapshot_failed: {exc}"}


def _load_telemetry_snapshot() -> dict[str, Any]:
    try:
        from src.telemetry.tracker import get_error_report, get_error_stats

        return {
            "stats": get_error_stats(),
            "recent_errors": get_error_report(limit=20),
        }
    except Exception as exc:
        return {
            "stats": {"total": 0, "by_type": {}, "recent_24h": 0},
            "recent_errors": [],
            "error": f"telemetry_snapshot_failed: {exc}",
        }


def _load_version_snapshot() -> dict[str, Any]:
    try:
        import updater as app_updater

        return {
            "display_version": app_updater.load_display_version(),
            "current_release": app_updater.get_current_release(),
            "candidate_release": app_updater.get_candidate_release(),
            "update_state": app_updater.load_update_state(),
        }
    except Exception as exc:
        return {"display_version": "unknown", "error": f"version_snapshot_failed: {exc}"}


def _build_summary_markdown(bundle: dict[str, Any]) -> str:
    health = bundle.get("health", {}) or {}
    summary = health.get("summary", {}) or {}
    telemetry = bundle.get("telemetry", {}) or {}
    stats = telemetry.get("stats", {}) or {}
    recent = bundle.get("recent_artifacts", {}) or {}
    return "\n".join(
        [
            "# GoldAutoTrade Diagnostics Summary",
            "",
            f"- bundle_id: {bundle.get('bundle_id', '')}",
            f"- created_at: {bundle.get('created_at', '')}",
            f"- hostname: {bundle.get('host', {}).get('hostname', '')}",
            f"- version: {bundle.get('version', {}).get('display_version', '')}",
            f"- health_ok: {health.get('ok', '')}",
            f"- alerts: {summary.get('alert_count', 0)}",
            f"- warning_count: {summary.get('warning_count', 0)}",
            f"- critical_count: {summary.get('critical_count', 0)}",
            f"- telemetry_total: {stats.get('total', 0)}",
            f"- telemetry_recent_24h: {stats.get('recent_24h', 0)}",
            f"- archived_requests: {len(recent.get('bridge_requests', []) or [])}",
            f"- archived_reports: {len(recent.get('bridge_reports', []) or [])}",
            "",
            "请优先检查:",
            "1. 当前交易运行态与 MT5/bridge 是否在线",
            "2. 最近请求与回执是否一致，是否存在拒单/超时/状态分叉",
            "3. 当前生效参数、风控阻断与最近错误是否解释本次异常",
            "",
        ]
    ) + "\n"


def create_diagnostic_bundle(
    *,
    app_dir: Path,
    data_dir: Path,
    logs_dir: Path,
    config_dir: Path,
    note: str = "",
    label: str = "manual",
) -> dict[str, Any]:
    diagnostics_dir = _diagnostics_dir(data_dir)
    bundle_id = f"diagnostics_{_now_compact()}"
    staging_dir = diagnostics_dir / bundle_id
    staging_dir.mkdir(parents=True, exist_ok=True)
    runtime_roots = _collect_runtime_roots(app_dir, data_dir, logs_dir, config_dir)


    archive_dirs = _runtime_archive_dirs(runtime_roots)
    bridge_requests = _recent_files_across_dirs(archive_dirs, "*.request.json")
    bridge_reports = _recent_files_across_dirs(archive_dirs, "*.report.json")

    recent_artifacts = {
        "bridge_requests": [path.name for path in bridge_requests],
        "bridge_reports": [path.name for path in bridge_reports],
    }
    summary_payload = {
        "bundle_id": bundle_id,
        "created_at": _now_iso(),
        "label": label,
        "note": note,
        "host": {
            "hostname": socket.gethostname(),
            "platform": platform.platform(),
            "python": platform.python_version(),
        },
        "paths": {
            "app_dir": str(app_dir),
            "data_dir": str(data_dir),
            "logs_dir": str(logs_dir),
            "config_dir": str(config_dir),
            "runtime_roots": [str(path) for path in runtime_roots],
        },
        "version": _load_version_snapshot(),
        "health": _load_health_snapshot(),
        "telemetry": _load_telemetry_snapshot(),
        "recent_artifacts": recent_artifacts,
    }
    summary_payload = redact_sensitive_data(summary_payload)

    summary_json_path = diagnostics_dir / f"{bundle_id}.summary.json"
    summary_markdown_path = diagnostics_dir / f"{bundle_id}.summary.md"
    _write_json(summary_json_path, summary_payload)
    _write_text(summary_markdown_path, _build_summary_markdown(summary_payload))

    _copy_binary_file(summary_json_path, staging_dir / "summary.json")
    _copy_binary_file(summary_markdown_path, staging_dir / "summary.md")

    copy_ops: list[tuple[str, bool]] = []
    resolved_sources: dict[str, str] = {}

    def _record(op_name: str, copied: bool, source: Path | None = None) -> None:
        copy_ops.append((op_name, copied))
        if copied and source is not None:
            resolved_sources[op_name] = str(source)

    def _copy_json_candidates(op_name: str, destination: Path, candidates: list[Path]) -> None:
        source = _freshest_existing_file(candidates)
        copied = _copy_redacted_json(source, destination) if source is not None else False
        _record(op_name, copied, source)

    def _copy_text_candidates(op_name: str, destination: Path, candidates: list[Path]) -> None:
        source = _freshest_existing_file(candidates)
        copied = _copy_text_tail(source, destination) if source is not None else False
        _record(op_name, copied, source)

    _copy_json_candidates(
        "app_config",
        staging_dir / "config" / "app_config.redacted.json",
        _runtime_root_candidates(runtime_roots, "app_config.json"),
    )
    _copy_json_candidates(
        "risk_governor_rules",
        staging_dir / "config" / "risk_governor_rules.json",
        _runtime_scope_candidates(runtime_roots, "config", "risk_governor_rules.json"),
    )
    _copy_json_candidates(
        "strategy_rules_v3",
        staging_dir / "config" / "strategy_rules_v3.json",
        _runtime_scope_candidates(runtime_roots, "config", "strategy_rules_v3.json"),
    )
    _copy_json_candidates(
        "strategy_market_state",
        staging_dir / "config" / "strategy_market_state.json",
        _runtime_scope_candidates(runtime_roots, "config", "strategy_market_state.json"),
    )

    for rel_path in (
        ("logs", "runtime.log"),
        ("logs", "decision_ledger.ndjson"),
        ("logs", "trading_ledger.ndjson"),
        ("logs", "risk_events.ndjson"),
        ("logs", "system_bus.ndjson"),
        ("logs", "runtime_scheduler_snapshot.json"),
        ("logs", "strategy_runtime_status.json"),
        ("logs", "effective_params.json"),
        ("logs", "performance_factors.json"),
        ("logs", "trailing_telemetry_latest.json"),
        ("telemetry", "errors.ndjson"),
        ("telemetry", "crashes.ndjson"),
        ("telemetry", "metrics.ndjson"),
    ):
        category, filename = rel_path
        dest = staging_dir / category / filename
        if filename.endswith(".json"):
            candidates = _runtime_scope_candidates(runtime_roots, "logs", filename)
            if category == "telemetry":
                candidates = _runtime_scope_candidates(runtime_roots, "data", "telemetry", filename)
            _copy_json_candidates(f"{category}/{filename}", dest, candidates)
        else:
            candidates = _runtime_scope_candidates(runtime_roots, "logs", filename)
            if category == "telemetry":
                candidates = _runtime_scope_candidates(runtime_roots, "data", "telemetry", filename)
            _copy_text_candidates(f"{category}/{filename}", dest, candidates)

    for op_name, candidates, dest in (
        (
            "state/agent_state.json",
            _runtime_scope_candidates(runtime_roots, "data", "bridge", "state", "agent_state.json"),
            staging_dir / "state" / "agent_state.json",
        ),
        (
            "state/terminal_status.json",
            _runtime_scope_candidates(runtime_roots, "data", "bridge", "state", "terminal_status.json"),
            staging_dir / "state" / "terminal_status.json",
        ),
        (
            "bridge_current/request.json",
            _runtime_scope_candidates(runtime_roots, "data", "inbox", "request.json"),
            staging_dir / "bridge_current" / "request.json",
        ),
        (
            "bridge_current/execution_report.json",
            _runtime_scope_candidates(runtime_roots, "data", "inbox", "execution_report.json"),
            staging_dir / "bridge_current" / "execution_report.json",
        ),
        (
            "bridge_current/signal.json",
            _runtime_scope_candidates(runtime_roots, "data", "outbox", "signal.json"),
            staging_dir / "bridge_current" / "signal.json",
        ),
        (
            "state/execution_state.json",
            _runtime_scope_candidates(runtime_roots, "data", "execution_state.json"),
            staging_dir / "state" / "execution_state.json",
        ),
        (
            "state/position_lifecycle.json",
            _runtime_scope_candidates(runtime_roots, "data", "position_lifecycle.json"),
            staging_dir / "state" / "position_lifecycle.json",
        ),
        (
            "state/blackboard_state.json",
            _runtime_root_candidates(runtime_roots, "blackboard_state.json"),
            staging_dir / "state" / "blackboard_state.json",
        ),
    ):
        _copy_json_candidates(op_name, dest, candidates)

    _copy_text_candidates(
        "logs/gold_trader.daemon.log",
        staging_dir / "logs" / "gold_trader.daemon.log",
        [
            *_runtime_scope_candidates(runtime_roots, "logs", "gold_trader.daemon.log"),
            *_runtime_scope_candidates(runtime_roots, "bridge", "logs", "gold_trader.daemon.log"),
        ],
    )
    _copy_text_candidates(
        "logs/lobsterquant_trader.log",
        staging_dir / "logs" / "lobsterquant_trader.log",
        _runtime_root_candidates(runtime_roots, "lobsterquant_trader.log"),
    )

    archive_staging_dir = staging_dir / "bridge_archive"
    for file_path in bridge_requests + bridge_reports:
        target = archive_staging_dir / file_path.name
        copied = _copy_redacted_json(file_path, target)
        _record(f"bridge_archive/{file_path.name}", copied, file_path)

    exported_files = [name for name, copied in copy_ops if copied]
    missing_files = [name for name, copied in copy_ops if not copied]
    _write_json(
        staging_dir / "manifest.json",
        {
            "bundle_id": bundle_id,
            "created_at": summary_payload.get("created_at", ""),
            "label": label,
            "note": note,
            "runtime_roots": [str(path) for path in runtime_roots],
            "exported_files": exported_files,
            "missing_files": missing_files,
            "resolved_sources": resolved_sources,
        },
    )

    archive_path = diagnostics_dir / f"{bundle_id}.zip"
    _zip_directory(staging_dir, archive_path)
    shutil.rmtree(staging_dir, ignore_errors=True)

    bundle = {
        "bundle_id": bundle_id,
        "created_at": summary_payload.get("created_at", ""),
        "label": label,
        "archive_path": str(archive_path),
        "archive_name": archive_path.name,
        "archive_size": archive_path.stat().st_size if archive_path.exists() else 0,
        "summary_json_path": str(summary_json_path),
        "summary_markdown_path": str(summary_markdown_path),
        "recent_artifacts": recent_artifacts,
    }
    _write_json(diagnostics_dir / f"{bundle_id}.bundle.json", bundle)
    _prune_diagnostic_bundles(diagnostics_dir)
    return bundle


