# SonicForge Runtime Profile — phát hiện môi trường chạy (desktop Windows /
# docker headless) và khả năng của máy, để app TỰ CHỌN cách xử lý:
#   - Windows desktop (server + client cùng 1 máy): Carla local để preview +
#     chỉnh preset → pedalboard render (Hướng A)
#   - Docker / Linux headless (server + browser UI): soundfont + VSTi mở được
#     từ storage mount; preset upload qua browser; không có GUI local.
#
# Ưu tiên: SF_RUNTIME env override > heuristic (platform + display + docker).
# Module CHỈ dùng stdlib + app.config (không kéo pedalboard/numpy) để import
# nhẹ và hoạt động trong mọi tiến trình (web/worker/celery).
import os
import sys
import json
import shutil
import socket
import functools

from app.config import settings

SF_RUNTIME_ENV = "SF_RUNTIME"  # auto | desktop | headless
SF_DOCKER_ENV = "SF_DOCKER"    # 1 = chạy trong container Docker
CARLA_CONFIG_FILE = "carla_path.json"  # storage/carla_path.json — user định vị
# Bản Carla portable (zip) có thể giải nén ở BẤT KỲ ĐÂU — PATH/Program Files
# không đủ. User tự chọn thư mục chứa carla.exe qua Plugin Manager → lưu file
# này (ưu tiên cao nhất khi detect), kèm tìm kiếm nông Downloads/Desktop.


def _is_windows() -> bool:
    return os.name == "nt"


def _is_macos() -> bool:
    return sys.platform == "darwin"


def _is_linux() -> bool:
    return not _is_windows() and not _is_macos()


def _in_docker() -> bool:
    if os.environ.get(SF_DOCKER_ENV) == "1":
        return True
    # Marker chuẩn của Docker (không tồn tại trên máy host thường)
    try:
        return os.path.exists("/.dockerenv")
    except Exception:
        return False


def _has_display() -> bool:
    """Có môi trường GUI hiển thị được hay không.

    Windows/macOS luôn có (desktop). Linux cần biến DISPLAY (X11) — nếu chạy
    Docker không có DISPLAY → headless."""
    if _is_windows() or _is_macos():
        return True
    return bool(os.environ.get("DISPLAY"))


def _tauri_bridge_ready() -> bool:
    """App desktop Tauri viết marker file lúc setup (xem plugins.py
    _pick_dir_via_tauri_bridge) — dùng để nhận diện bản desktop app."""
    try:
        root = os.environ.get("APPDATA") or os.path.expanduser("~")
        return os.path.exists(os.path.join(root, "SonicForgeDAW", "ipc", "tauri_bridge_ready"))
    except Exception:
        return False


def _carla_config_path() -> str:
    return os.path.join(settings.STORAGE_DIR, CARLA_CONFIG_FILE)


def get_configured_carla() -> str:
    """Đường dẫn carla.exe do USER tự định vị (lưu trong storage/carla_path.json).

    File lưu đường dẫn tới carla.exe (đã resolve) hoặc thư mục chứa — nếu là
    thư mục, tìm carla.exe bên trong (độ sâu ≤ 2). Đây là cách bắt buộc có cho
    bản Carla portable (zip) giải nén ở vị trí bất kỳ, không cài đặt/PATH."""
    try:
        with open(_carla_config_path(), "r", encoding="utf-8") as f:
            data = json.load(f)
    except Exception:
        return ""
    p = (data.get("carla_path") or "").strip()
    if not p:
        return ""
    if os.path.isfile(p) and os.path.basename(p).lower() in ("carla.exe", "carla-single.exe", "carla"):
        return p
    if os.path.isdir(p):
        try:
            for root, dirs, files in os.walk(p):
                depth = root[len(p):].count(os.sep)
                if depth >= 2:
                    dirs[:] = []
                    continue
                for f in files:
                    if f.lower() == "carla.exe":
                        return os.path.join(root, f)
        except Exception:
            pass
    return ""


def save_carla_path(path: str) -> str:
    """Lưu vị trí Carla do user chọn (Plugin Manager → Định vị Carla...).

    Chấp nhận: đường dẫn tới carla.exe, hoặc thư mục chứa carla.exe (portable
    zip). Resolve về đường dẫn exe hợp lệ → ghi config → xóa cache detect.
    Trả đường dẫn exe, hoặc '' nếu không hợp lệ."""
    path = (path or "").strip().strip('"').strip()
    exe = ""
    if os.path.isfile(path) and os.path.basename(path).lower() in ("carla.exe", "carla-single.exe", "carla"):
        exe = path
    elif os.path.isdir(path):
        try:
            for root, dirs, files in os.walk(path):
                depth = root[len(path):].count(os.sep)
                if depth >= 2:
                    dirs[:] = []
                    continue
                for f in files:
                    if f.lower() == "carla.exe":
                        exe = os.path.join(root, f)
                        break
                if exe:
                    break
        except Exception:
            pass
    if not exe:
        return ""
    try:
        os.makedirs(settings.STORAGE_DIR, exist_ok=True)
        with open(_carla_config_path(), "w", encoding="utf-8") as f:
            json.dump({"carla_path": exe, "carla_exe": os.path.basename(exe)}, f, ensure_ascii=False, indent=2)
    except Exception:
        return ""
    # Xóa cache detect/find_carla để capabilities phản ánh ngay
    invalidate_carla_cache()
    return exe


def _carla_from_registry() -> str:
    """Windows: Carla cài qua installer có thể ghi registry (best-effort)."""
    if not _is_windows():
        return ""
    try:
        import winreg
        for hive, key in (
            (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Carla"),
            (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Carla"),
            (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Carla"),
        ):
            try:
                with winreg.OpenKey(hive, key) as k:
                    val, _ = winreg.QueryValueEx(k, "InstallPath")
                    p = os.path.join(str(val), "carla.exe")
                    if os.path.isfile(p):
                        return p
            except Exception:
                continue
    except Exception:
        pass
    return ""


def _find_carla_exe_bounded(base: str, max_depth: int = 3) -> str:
    """Quét nông tìm carla.exe (bản portable giải nén thường nằm Downloads/
    Desktop/Documents). Giới hạn độ sâu + bỏ qua thư mục lớn — KHÔNG quét
    toàn ổ đĩa."""
    if not base or not os.path.isdir(base):
        return ""
    skip = ("node_modules", ".cache", "AppData", ".git", "venv", ".venv",
            "__pycache__", "$RECYCLE.BIN", "Windows", "Program Files",
            "Program Files (x86)")
    try:
        for root, dirs, files in os.walk(base):
            depth = root[len(base):].count(os.sep) if root != base else 0
            if depth >= max_depth:
                dirs[:] = []
                continue
            for f in files:
                if f.lower() == "carla.exe":
                    return os.path.join(root, f)
            dirs[:] = [d for d in dirs if d not in skip and not d.startswith(".")]
    except Exception:
        pass
    return ""


def _carla_from_shallow_search() -> str:
    """Windows: tìm carla.exe trong Downloads/Desktop/Documents/Home."""
    if not _is_windows():
        return ""
    home = os.environ.get("USERPROFILE") or os.path.expanduser("~")
    bases = []
    for sub in ("Downloads", "Desktop", "Documents"):
        bases.append(os.path.join(home, sub))
    bases.append(home)
    for base in bases:
        hit = _find_carla_exe_bounded(base)
        if hit:
            return hit
    return ""


def _carla_candidates() -> list:
    """Các vị trí Carla — theo thứ tự ưu tiên:
    1) user định vị (config file — bản portable bắt buộc dùng cách này)
    2) PATH (shutil.which)
    3) registry (Windows, nếu cài qua installer)
    4) thư mục cài đặt chuẩn (Program Files...)
    5) quét nông Downloads/Desktop/Documents (Windows) / AppImage (Linux)"""
    cands = []
    configured = get_configured_carla()
    if configured:
        cands.append(configured)
    try:
        which = shutil.which("carla")
        if which:
            cands.append(which)
    except Exception:
        pass
    reg = _carla_from_registry()
    if reg:
        cands.append(reg)
    try:
        if _is_windows():
            pf = os.environ.get("ProgramFiles", r"C:\Program Files")
            lpf = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
            la = os.environ.get("LOCALAPPDATA", "")
            for base in (pf, lpf, la):
                for rel in ("Carla\\carla.exe", "Carla\\bin\\carla.exe", "Programs\\Carla\\carla.exe"):
                    p = os.path.join(base, rel) if base else ""
                    if p and os.path.isfile(p):
                        cands.append(p)
        elif _is_linux():
            home = os.path.expanduser("~")
            for p in ("/usr/bin/carla", "/usr/local/bin/carla", "/opt/carla/carla"):
                if os.path.isfile(p):
                    cands.append(p)
            try:
                for f in os.listdir(home):
                    if f.lower().startswith("carla") and f.lower().endswith(".appimage"):
                        cands.append(os.path.join(home, f))
            except Exception:
                pass
        elif _is_macos():
            for p in ("/Applications/Carla.app/Contents/MacOS/Carla", "/Applications/Carla.app/Contents/MacOS/carla"):
                if os.path.isfile(p):
                    cands.append(p)
    except Exception:
        pass
    shallow = _carla_from_shallow_search()
    if shallow:
        cands.append(shallow)
    # Dedup giữ thứ tự
    seen, out = set(), []
    for c in cands:
        if c not in seen:
            seen.add(c)
            out.append(c)
    return out


@functools.lru_cache(maxsize=1)
def find_carla() -> str:
    """Đường dẫn Carla đầu tiên tìm thấy, hoặc '' nếu chưa có.

    Lưu ý license: Carla GPL-2.0+ — app KHÔNG bundle/nhúng, chỉ spawn tiến
    trình ngoài (user tự cài/giải nén) → không dính copyleft."""
    for c in _carla_candidates():
        return c
    return ""


def invalidate_carla_cache():
    """Xóa cache detect/find_carla — gọi sau khi user định vị Carla mới."""
    find_carla.cache_clear()
    detect.cache_clear()


def _detect_platform() -> str:
    if _is_windows():
        return "windows"
    if _is_macos():
        return "darwin"
    return "linux"


def _detect_runtime() -> str:
    """auto → desktop nếu có GUI (Windows/macOS luôn; Linux cần DISPLAY và
    không phải docker), ngược lại headless. SF_RUNTIME override thắng."""
    override = (os.environ.get(SF_RUNTIME_ENV) or "auto").strip().lower()
    if override in ("desktop", "headless"):
        return override
    if _is_windows() or _is_macos():
        return "desktop"
    if _in_docker() or not _has_display():
        return "headless"
    return "desktop"


def default_vst_dirs() -> list:
    """Thư mục VST mặc định theo platform (vẫn ưu tiên env VST_DIR).

    Windows: thư mục chuẩn VST3 của hệ thống; user thường khai báo thêm qua
    Plugins Manager (plugin_dirs.json)."""
    if settings.VST_DIR and settings.VST_DIR != "/opt/daw_engine/vst3":
        return [settings.VST_DIR]
    if _is_windows():
        pf = os.environ.get("ProgramFiles", r"C:\Program Files")
        return [os.path.join(pf, "Common Files", "VST3"), os.path.join(pf, "VSTPlugins")]
    if _is_macos():
        return ["/Library/Audio/Plug-Ins/VST3", os.path.expanduser("~/Library/Audio/Plug-Ins/VST3")]
    return ["/opt/daw_engine/vst3", os.path.expanduser("~/.vst3")]


def default_soundfont_dirs() -> list:
    if settings.SOUNDFONT_DIR and settings.SOUNDFONT_DIR != "/opt/daw_engine/soundfonts":
        return [settings.SOUNDFONT_DIR]
    if _is_windows():
        root = os.environ.get("APPDATA") or os.path.expanduser("~")
        return [os.path.join(root, "SonicForgeDAW", "soundfonts")]
    if _is_macos():
        return [os.path.expanduser("~/Music/SonicForgeDAW/soundfonts")]
    return ["/opt/daw_engine/soundfonts", os.path.expanduser("~/.sf2")]


def _lan_ips() -> list:
    """IPv4 của máy trên mạng LAN (bỏ loopback) — để client browser ở máy
    khác mở app qua địa chỉ này. Stdlib only (không kéo thư viện ngoài)."""
    ips = []
    try:
        # IP ra mạng theo default route — không gửi gói tin thật
        with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
        if ip and not ip.startswith("127.") and ip not in ips:
            ips.append(ip)
    except Exception:
        pass
    # Enumerate thêm các interface khác (multi-NIC)
    try:
        for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
            ip = info[4][0]
            if ip and not ip.startswith("127.") and ip not in ips:
                ips.append(ip)
    except Exception:
        pass
    return ips

@functools.lru_cache(maxsize=1)
def detect() -> dict:
    """Detect 1 lần (cache toàn cục) — kết quả bất biến trong 1 tiến trình."""
    platform = _detect_platform()
    runtime = _detect_runtime()
    carla = find_carla() if runtime == "desktop" else ""
    docker = _in_docker()
    return {
        "platform": platform,
        "runtime": runtime,                 # "desktop" | "headless"
        "docker": docker,
        "environment": "docker" if docker else "standalone",  # luồng âm instrument: docker→FluidSynthWASM, standalone→native OS
        "has_display": _has_display(),
        "tauri_bridge": _tauri_bridge_ready(),
        "carla_path": carla,
        "carla_local": bool(carla),         # chỉ có ý nghĩa khi runtime=desktop
        "vst_render": _vst_render_available(),
        "default_vst_dirs": default_vst_dirs(),
        "default_soundfont_dirs": default_soundfont_dirs(),
        "preset_dir": os.path.join(settings.STORAGE_DIR, "presets"),
    }


def _vst_render_available() -> bool:
    """pedalboard có sẵn không (dùng find_spec — KHÔNG import pedalboard để
    tránh kéo JUCE lib; giống pattern _module_available trong vst_engine)."""
    import importlib.util
    try:
        return importlib.util.find_spec("pedalboard") is not None
    except (ImportError, AttributeError, ValueError):
        return False


def _lan_info() -> dict:
    """Địa chỉ + port để client LAN mở app. Port: SF_PORT (desktop engine chọn
    trước, 8000-8010) hoặc 8000 mặc định."""
    try:
        port = int(os.environ.get("SF_PORT") or "8000")
    except ValueError:
        port = 8000
    ips = _lan_ips()
    return {
        "ips": ips,
        "port": port,
        "urls": [f"http://{ip}:{port}" for ip in ips],
    }

def capabilities() -> dict:
    """Capabilities API — frontend gọi 1 lần lúc boot để bật/tắt tính năng."""
    d = detect()
    features = {
        "carla_local": d["carla_local"],
        "carla_path": d["carla_path"],
        "preset_upload": True,              # cả 2 mode đều upload preset được
        "vst_render": d["vst_render"],
        "tauri_bridge": d["tauri_bridge"],
        # preview VSTi: quick_render (pedalboard render clip ngắn — âm thật,
        # giống export) là chuẩn cho cả 2 mode; không có realtime trong web UI.
        "preview_mode": "quick_render" if d["vst_render"] else "wasm",
    }
    return {
        "success": True,
        "runtime": d["runtime"],
        "platform": d["platform"],
        "docker": d["docker"],
        "environment": d["environment"],
        # LAN access (chỉ standalone): URL để client browser ở máy khác trên
        # mạng mở app. Docker (container) → bỏ qua (IP container không dùng được).
        **({"lan": _lan_info()} if not d["docker"] else {}),
        "features": features,
        "default_dirs": {
            "vst": d["default_vst_dirs"],
            "soundfont": d["default_soundfont_dirs"],
            "preset": d["preset_dir"],
        },
    }


def is_desktop() -> bool:
    return detect()["runtime"] == "desktop"


def is_headless() -> bool:
    return detect()["runtime"] == "headless"


if __name__ == "__main__":
    # python -m app.core.runtime  → in capabilities để kiểm tra detect
    import json
    print(json.dumps(capabilities(), ensure_ascii=False, indent=2))
