# SonicForge Studio VST / VSTi Engine Service
import os
import json
import numpy as np
import functools
from ctypes import c_char_p

def midi_note_to_freq(note_number: int) -> float:
    return 440.0 * (2.0 ** ((note_number - 69) / 12.0))


def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = 0,
                                   program: int = 0, sr: int = 44100, bpm: float = 120.0) -> np.ndarray:
    """Native FluidSynth (pyfluidsynth) — render MIDI events bằng soundfont THẬT.

    Dùng cho môi trường standalone: âm instrument xử lí trực tiếp trên OS
    (backend pyfluidsynth), không phải FluidSynthWASM trong browser.
    Xử lí đúng note CHỒNG nhau + đuôi release (event-stream noteon/noteoff).
    Trả stereo float32 (2, N). Raise RuntimeError nếu pyfluidsynth thiếu / SF
    không load được."""
    import fluidsynth as _fs
    settings = _fs.new_fluid_settings()
    _fs.fluid_settings_setnum(settings, b'synth.sample-rate', float(sr))
    synth = _fs.new_fluid_synth(settings)
    try:
        fid = _fs.fluid_synth_sfload(synth, os.fspath(sf_path).encode('utf-8'), 1)
        if fid == -1:
            raise RuntimeError(f"SoundFont load failed: {sf_path}")
        _fs.fluid_synth_program_select(synth, 0, fid, int(bank), int(program))
        beat_sec = 60.0 / max(30.0, bpm)
        events = []
        total_sec = 0.0
        for ev in midi_events:
            start_s = int(float(ev.get('start_beat', 0)) * beat_sec * sr)
            dur_s = int(float(ev.get('duration_beats', 1)) * beat_sec * sr)
            note = int(ev.get('note', 60))
            vel = min(127, max(1, int(float(ev.get('velocity', 100)))))
            events.append((start_s, 'on', note, vel))
            events.append((start_s + dur_s, 'off', note, 0))
            end_sec = (start_s + dur_s) / float(sr)
            if end_sec > total_sec:
                total_sec = end_sec
        events.sort(key=lambda e: e[0])
        total_sec = max(total_sec, 0.25) + 0.5  # đuôi reverb/release
        n = int(total_sec * sr)
        out = np.zeros((2, n), dtype=np.float32)
        pos = 0

        def _render_until(target: int):
            """Render từ pos tới target, CỘNG vào out (giữ âm đang ngân)."""
            nonlocal pos
            target = min(target, n)
            while pos < target:
                chunk = min(target - pos, 44100)
                block = _fs.fluid_synth_write_s16_stereo(synth, chunk)
                b = block.astype(np.float32).reshape(-1, 2).T / 32768.0
                take = min(chunk, b.shape[1], target - pos)
                if take > 0:
                    out[:, pos:pos + take] += b[:, :take]
                pos += take

        for t, kind, note, vel in events:
            if t > pos:
                _render_until(t)
            if pos >= n:
                break
            if kind == 'on':
                _fs.fluid_synth_noteon(synth, 0, note, vel)
            else:
                _fs.fluid_synth_noteoff(synth, 0, note)
        _render_until(n)  # đuôi release của notes cuối
        return out
    finally:
        try:
            _fs.delete_fluid_synth(synth)
        except Exception:
            pass

def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray:
    beat_duration_sec = 60.0 / max(30.0, bpm)
    max_duration_sec = 2.0
    for event in midi_events:
        start_beat = event.get('start_beat', 0.0)
        dur_beats = event.get('duration_beats', 1.0)
        end_sec = (start_beat + dur_beats) * beat_duration_sec
        if end_sec > max_duration_sec:
            max_duration_sec = end_sec
    total_samples = int((max_duration_sec + 0.5) * sr)
    out_l = np.zeros(total_samples, dtype=np.float32)
    out_r = np.zeros(total_samples, dtype=np.float32)
    for event in midi_events:
        note = event.get('note', 60)
        velocity = event.get('velocity', 100) / 127.0
        start_beat = event.get('start_beat', 0.0)
        dur_beats = event.get('duration_beats', 1.0)
        start_sample = int(start_beat * beat_duration_sec * sr)
        dur_samples = int(dur_beats * beat_duration_sec * sr)
        end_sample = min(total_samples, start_sample + dur_samples)
        actual_len = end_sample - start_sample
        if actual_len <= 0 or start_sample >= total_samples:
            continue
        freq = midi_note_to_freq(note)
        t = np.arange(actual_len) / float(sr)
        tone = 0.6 * np.sin(2 * np.pi * freq * t) + 0.3 * np.sin(2 * np.pi * freq * 2 * t) + 0.1 * np.sin(2 * np.pi * freq * 3 * t)
        attack = min(int(0.01 * sr), actual_len // 4)
        release = min(int(0.05 * sr), actual_len // 4)
        env = np.ones(actual_len, dtype=np.float32)
        if attack > 0:
            env[:attack] = np.linspace(0.0, 1.0, attack)
        if release > 0:
            env[-release:] = np.linspace(1.0, 0.0, release)
        signal = tone * env * velocity
        out_l[start_sample:end_sample] += signal
        out_r[start_sample:end_sample] += signal
    max_peak = max(np.max(np.abs(out_l)), np.max(np.abs(out_r)))
    if max_peak > 1.0:
        out_l /= max_peak
        out_r /= max_peak
    return np.vstack([out_l, out_r])

def _module_available(name: str) -> bool:
    """Kiem tra module co san khong — KHONG spawn subprocess.

    Truoc day dung subprocess.run([sys.executable, '-c', 'import X']) —
    khi app dong goi (PyInstaller frozen), sys.executable = daw_engine.exe
    -> subprocess chay CA ENGINE (bootloader bo qua '-c', chay desktop_engine)
    -> moi lan check lai sinh ra engine moi -> de quy spawn vo han
    (Task Manager day daw_engine, port 8000-8005 leo thang, load rat cham).
    find_spec() nhanh (micro-giay) va hoat dong ca source lan frozen.
    """
    import importlib.util
    try:
        return importlib.util.find_spec(name) is not None
    except (ImportError, AttributeError, ValueError):
        return False


def check_pedalboard_safe():
    return _module_available("pedalboard")


def check_pyfluidsynth_safe():
    return _module_available("fluidsynth")

HAS_PEDALBOARD = check_pedalboard_safe()
HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()

def ensure_pyfluidsynth():
    global HAS_PYFLUIDSYNTH
    if not HAS_PYFLUIDSYNTH:
        HAS_PYFLUIDSYNTH = check_pyfluidsynth_safe()
    return HAS_PYFLUIDSYNTH

if HAS_PEDALBOARD:
    try:
        from pedalboard import VST3Plugin, Pedalboard, Gain
    except Exception:
        HAS_PEDALBOARD = False

if HAS_PYFLUIDSYNTH:
    try:
        import fluidsynth
    except Exception:
        HAS_PYFLUIDSYNTH = False


# ── Module-level caches ──
_FLUID_CACHE = {}  # path → (fluidsynth.FluidSynth, refcount)
_PLUGIN_MANAGER_INSTANCE = None
_PLUGIN_MANAGER_ARGS = None
_SF_INSTRUMENTS_CACHE = {}  # sf_id → list[presets]

def _load_user_plugin_dirs() -> list:
    """Đọc plugin_dirs.json (Plugin Manager user chọn) — cùng file với
    plugins.py (STORAGE_DIR/plugin_dirs.json). Không import plugins.py để
    tránh vòng import (plugins.py import vst_engine)."""
    try:
        from app.config import settings as _st
        path = os.path.join(_st.STORAGE_DIR, "plugin_dirs.json")
        if os.path.exists(path):
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            return [d for d in (data.get("plugin_dirs") or []) if d]
    except Exception:
        pass
    return []


def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "PluginManager":
    """Singleton: reuse PluginManager when args match, else create new.
    Default dirs từ settings (env/.env/docker-compose hoặc user override).
    VST scan gộp thêm plugin_dirs user (Plugin Manager) — nút Synth phải liệt
    kê được VSTi đã scan và load_vst phải tìm thấy chúng khi render."""
    from app.config import settings as _st
    vst_dir = vst_dir or _st.VST_DIR
    sf_dir = sf_dir or _st.SOUNDFONT_DIR
    upload_sf_dir = upload_sf_dir or _st.STORAGE_DIR + "/soundfonts"
    extra = _load_user_plugin_dirs()
    global _PLUGIN_MANAGER_INSTANCE, _PLUGIN_MANAGER_ARGS
    args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra))
    if _PLUGIN_MANAGER_INSTANCE is not None and _PLUGIN_MANAGER_ARGS == args:
        return _PLUGIN_MANAGER_INSTANCE
    _PLUGIN_MANAGER_ARGS = args
    _PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra)
    return _PLUGIN_MANAGER_INSTANCE

def load_soundfont_cached(path: str):
    """Return a cached low-level FluidSynth instance for path, incrementing refcount.

    Uses the CFFI binding API (new_fluid_synth / fluid_synth_sfload) — the same
    API render_engine relies on. The high-level `FluidSynth()`/`Synth()` classes
    do not exist in this binding, so they are never used here.
    """
    global _FLUID_CACHE
    if not HAS_PYFLUIDSYNTH:
        return None
    if path in _FLUID_CACHE:
        fl, ref = _FLUID_CACHE[path]
        _FLUID_CACHE[path] = (fl, ref + 1)
        return fl
    try:
        import fluidsynth as _fs
        _settings = _fs.new_fluid_settings()
        _fs.fluid_settings_setnum(_settings, b'synth.sample-rate', 44100.0)
        fl = _fs.new_fluid_synth(_settings)
        font_id = _fs.fluid_synth_sfload(fl, path.encode("utf-8"), 1)
        if font_id < 0:
            _fs.delete_fluid_synth(fl)
            return None
        _fs.fluid_synth_program_select(fl, 0, font_id, 0, 0)
        _FLUID_CACHE[path] = (fl, 1)
        return fl
    except Exception:
        return None

def release_soundfont(path: str):
    """Decrement refcount; delete FluidSynth when count reaches 0."""
    global _FLUID_CACHE
    if path not in _FLUID_CACHE:
        return
    fl, ref = _FLUID_CACHE[path]
    if ref <= 1:
        try:
            import fluidsynth as _fs
            _fs.delete_fluid_synth(fl)
        except Exception:
            pass
        del _FLUID_CACHE[path]
    else:
        _FLUID_CACHE[path] = (fl, ref - 1)

class PluginManager:
    def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None):
        from app.config import settings as _st
        self.vst_dir = vst_dir or _st.VST_DIR
        self.sf_dir = sf_dir or _st.SOUNDFONT_DIR
        self.upload_sf_dir = upload_sf_dir
        # Thư mục VST thêm (plugin_dirs user scan trong Plugin Manager) —
        # list_available()/load_vst phải thấy VSTi user đã scan (bug: nút
        # Synth chỉ quét vst_dir env mặc định /opt/daw_engine/vst3).
        self.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d]
        self._sf_scan_cache = None  # cache for _scan_soundfonts()

    def _scan_plugins(self) -> dict:
        plugins = {}
        for scan_dir in [self.vst_dir] + self.extra_vst_dirs:
            if not scan_dir or not os.path.isdir(scan_dir):
                continue
            for root, dirs, files in os.walk(scan_dir):
                # Windows: VST3 là FOLDER tên X.vst3 (chứa X.vst3.dll bên trong)
                for d in list(dirs):
                    if d.lower().endswith(".vst3"):
                        plugins[os.path.splitext(d)[0]] = os.path.join(root, d)
                for file in files:
                    low = file.lower()
                    if low.endswith(".vst3") or low.endswith(".so") or low.endswith(".dll"):
                        plugin_path = os.path.join(root, file)
                        plugin_name = os.path.splitext(file)[0]
                        if plugin_name not in plugins:
                            plugins[plugin_name] = plugin_path
        return plugins

    def _scan_soundfonts(self) -> list:
        sf_map = {}
        dirs = [("system", self.sf_dir)]
        if self.upload_sf_dir and self.upload_sf_dir != self.sf_dir:
            dirs.append(("upload", self.upload_sf_dir))
        
        meta_cache = {}
        if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir):
            for f in os.listdir(self.upload_sf_dir):
                if f.endswith(".meta"):
                    try:
                        import json
                        with open(os.path.join(self.upload_sf_dir, f), "r") as mf:
                            meta_cache[os.path.splitext(f)[0]] = json.load(mf)
                    except Exception:
                        pass
        
        for source, d in dirs:
            if not os.path.isdir(d):
                continue
            for f in os.listdir(d):
                if f.endswith(".sf2") or f.endswith(".sf3"):
                    base_id = os.path.splitext(f)[0]
                    if base_id in sf_map:
                        continue
                    meta = meta_cache.get(base_id, None)
                    if meta:
                        display_name = meta.get("original_name", f)
                    else:
                        short_id = base_id[:8] if len(base_id) > 8 else base_id
                        display_name = f"SoundFont_{short_id}"
                    sf_map[base_id] = {
                        "id": base_id,
                        "name": display_name,
                        "file": f,
                        "display": os.path.splitext(display_name)[0][:40],
                        "source": source
                    }
        return list(sf_map.values())

    def load_vst(self, plugin_name: str, preset_data: dict = None):
        if not HAS_PEDALBOARD:
            return None
        plugins = self._scan_plugins()
        if plugin_name not in plugins:
            return None
        path = plugins[plugin_name]
        vst = VST3Plugin(path)
        if preset_data:
            for k, v in preset_data.items():
                try:
                    setattr(vst, k, v)
                except Exception:
                    pass
        return vst

    def _scan_soundfonts_cached(self):
        if self._sf_scan_cache is not None:
            return self._sf_scan_cache
        self._sf_scan_cache = self._scan_soundfonts()
        return self._sf_scan_cache

    def load_soundfont(self, path: str):
        return load_soundfont_cached(path)

    def list_soundfont_instruments(self, sf_id: str):
        if sf_id in _SF_INSTRUMENTS_CACHE:
            return _SF_INSTRUMENTS_CACHE[sf_id]
        search_dirs = []
        if os.path.isdir(self.sf_dir):
            search_dirs.append(self.sf_dir)
        if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir) and self.upload_sf_dir != self.sf_dir:
            search_dirs.append(self.upload_sf_dir)
        # Thư mục user thêm qua Plugin Manager (plugin_dirs — chứa cả VST lẫn
        # SoundFont): nếu không có, instrument của soundfont trong thư mục user
        # không liệt kê được (bug "nhấn tên SF không thấy instrument").
        for d in (self.extra_vst_dirs or []):
            if d and os.path.isdir(d) and d not in search_dirs:
                search_dirs.append(d)
        presets = []
        # ── 1) FluidSynth (nếu có lib) — đọc bank/program/name từ engine ──
        if ensure_pyfluidsynth():
            for d in search_dirs:
                if presets:
                    break
                for f in os.listdir(d):
                    if not (f.endswith(".sf2") or f.endswith(".sf3")):
                        continue
                    base = os.path.splitext(f)[0]
                    if base != sf_id and base != sf_id.replace("sf_", ""):
                        continue
                    path = os.path.join(d, f)
                    try:
                        import fluidsynth as _fs
                        # Low-level CFFI API (same as render_engine); never use
                        # the high-level Synth() class that this binding lacks.
                        _settings = _fs.new_fluid_settings()
                        _synth = _fs.new_fluid_synth(_settings)
                        try:
                            fid = _fs.fluid_synth_sfload(_synth, path.encode("utf-8"), 1)
                            if fid >= 0:
                                sfont = _fs.fluid_synth_get_sfont_by_id(_synth, fid)
                                if sfont:
                                    for bank in range(0, 2):
                                        for prog_num in range(0, 128):
                                            try:
                                                preset = _fs.fluid_sfont_get_preset(sfont, bank, prog_num)
                                            except Exception:
                                                break
                                            if preset:
                                                try:
                                                    name_ptr = _fs.fluid_preset_get_name(preset)
                                                    if name_ptr:
                                                        if hasattr(_fs, "ffi"):
                                                            raw = _fs.ffi.string(name_ptr)
                                                        else:
                                                            raw = c_char_p(name_ptr).value
                                                        if raw:
                                                            presets.append({
                                                                "bank": bank,
                                                                "program": prog_num,
                                                                "name": raw.decode("utf-8", errors="replace"),
                                                            })
                                                except Exception:
                                                    continue
                        finally:
                            try:
                                _fs.delete_fluid_synth(_synth)
                            except Exception:
                                pass
                    except Exception:
                        import traceback; traceback.print_exc()
                    break  # đã xử lý file khớp
        # ── 2) Fallback: sf2utils đọc TRỰC TIẾP file (thuần Python) ──
        # Không cần libfluidsynth — quan trọng trên Windows khi thiếu DLL.
        # Đọc preset header (pdta/phdr) → bank/program/name như nhau.
        if not presets:
            try:
                from app.core.soundfont_inspector import SoundFontInspector
                insp = SoundFontInspector(system_sf_dir=self.sf_dir, upload_sf_dir=self.upload_sf_dir)
                for d in search_dirs:
                    if presets:
                        break
                    for f in os.listdir(d):
                        if not (f.endswith(".sf2") or f.endswith(".sf3")):
                            continue
                        base = os.path.splitext(f)[0]
                        if base == sf_id or base == sf_id.replace("sf_", ""):
                            info = insp.inspect_sf2_file(os.path.join(d, f))
                            if info and info.get("instruments"):
                                presets = info["instruments"]
                            break
            except Exception:
                presets = []
        _SF_INSTRUMENTS_CACHE[sf_id] = presets[:256]
        return presets[:256]

    def list_available(self) -> dict:
        return {
            "vst_instruments": [
                {"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
                for k in self._scan_plugins().keys()
            ],
            "soundfonts": self._scan_soundfonts()
        }

    @staticmethod
    def midi_events_to_messages(midi_events: list, bpm: float, sr: int, bank: int = None, program: int = None) -> list:
        if not HAS_PEDALBOARD:
            return []
        beat_duration_sec = 60.0 / max(30.0, bpm)
        # pedalboard >= 0.9: MIDI messages là tuple (bytes, timestamp_seconds)
        # (MidiMessage class đã bị bỏ). bytes là raw MIDI event:
        #   0x9n note_on, 0x8n note_off, 0xB0 control_change, 0xC0 program_change
        messages = []
        if bank is not None:
            messages.append((bytes([0xB0, 0, int(bank) & 0x7F]), 0.0))
        if program is not None:
            messages.append((bytes([0xC0, int(program) & 0x7F]), 0.0))
        for ev in midi_events:
            note = int(ev.get("note", 60)) & 0x7F
            velocity = max(0, min(127, int(ev.get("velocity", 100))))
            start_beat = float(ev.get("start_beat", 0.0))
            dur_beats = float(ev.get("duration_beats", 1.0))
            start_sec = start_beat * beat_duration_sec
            end_sec = (start_beat + dur_beats) * beat_duration_sec
            messages.append((bytes([0x90, note, velocity]), start_sec))
            messages.append((bytes([0x80, note, 0]), end_sec))
        return messages

    @staticmethod
    def validate_sf2_header(data: bytes) -> bool:
        if len(data) < 12:
            return False
        if data[0:4] != b'RIFF':
            return False
        if data[8:12] != b'sfbk':
            return False
        return True


class DecentSamplerManager:
    def __init__(self, vst_path="/opt/daw_engine/vst3/DecentSampler.vst3"):
        self.vst_path = vst_path

    def create_decent_sampler_instance(self, dspreset_path: str):
        if not HAS_PEDALBOARD:
            raise RuntimeError("pedalboard not available")
        if not os.path.exists(self.vst_path):
            raise FileNotFoundError(f"DecentSampler VST3 not found at {self.vst_path}")
        if not os.path.exists(dspreset_path):
            raise FileNotFoundError(f"Preset file not found at {dspreset_path}")

        plugin = VST3Plugin(self.vst_path)

        abs_preset = os.path.abspath(dspreset_path)
        preset_dir = os.path.dirname(abs_preset)
        cwd_before = os.getcwd()
        try:
            os.chdir(preset_dir)
            plugin.load_preset(abs_preset)
        finally:
            os.chdir(cwd_before)

        return plugin


# ── Preset bridge (Carla ↔ pedalboard) ─────────────────────────────────────
# Carla (chạy ngoài, user tự cài — GPL-2.0+ nên app KHÔNG bundle/nhúng) dùng
# để mở native GUI VSTi + xuất file preset (.vstpreset từ nút Save của plugin).
# File preset nằm trong thư viện storage/presets (upload qua web UI hoặc picker
# local trên Windows) → render_engine tải qua apply_preset_to_plugin() khi render.
PRESET_EXTENSIONS = (".vstpreset", ".fxp", ".fxb", ".dspreset")


def preset_library_dir() -> str:
    from app.config import settings as _st
    d = os.path.join(_st.STORAGE_DIR, "presets")
    try:
        os.makedirs(d, exist_ok=True)
    except Exception:
        pass
    return d


def resolve_preset_path(preset_id_or_path: str) -> str:
    """preset_id (tên file trong thư viện) hoặc đường dẫn tuyệt đối → path.

    Trả '' nếu không tìm thấy. Chống path traversal: chỉ chấp nhận tên file
    (không chứa separator) hoặc đường dẫn tuyệt đối tồn tại."""
    if not preset_id_or_path:
        return ""
    p = preset_id_or_path
    # Đường dẫn tuyệt đối / tương đối tồn tại → dùng thẳng
    if os.path.isfile(p):
        return p
    # id dạng tên file trong thư viện (uuid + ext)
    if os.path.basename(p) == p:
        cand = os.path.join(preset_library_dir(), p)
        if os.path.isfile(cand):
            return cand
        # Không có ext → quét theo prefix (uuid.idx → uuid.vstpreset)
        try:
            for f in os.listdir(preset_library_dir()):
                if f.lower().endswith(PRESET_EXTENSIONS) and f.startswith(p + "."):
                    return os.path.join(preset_library_dir(), f)
        except Exception:
            pass
    return ""


def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
    """E3: resolve path thật của plugin/soundfont cho Native Bridge (E2).
    ext: '.vst3' | '.vst2' | '.sf2' | '.sf3' | '.sfz' — filter phần mở rộng
    khi quét plugin_dirs. Trả '' nếu không tìm thấy. Chống path traversal:
    chỉ tên file (không separator) hoặc path tồn tại được chấp nhận."""
    if not name_or_path:
        return ""
    p = name_or_path
    if os.path.isfile(p):
        return p
    base = os.path.basename(p.replace("\\", "/"))
    base_noext = os.path.splitext(base)[0].lower()
    want_ext = ext.lower() if ext else None
    dirs = []
    try:
        dirs = list(_load_user_plugin_dirs()) or []
    except Exception:
        pass
    dirs += [settings.VST_DIR, settings.SOUNDFONT_DIR]
    for base_dir in dirs:
        if not os.path.isdir(base_dir):
            continue
        for root, _, files in os.walk(base_dir):
            for fn in files:
                if want_ext and not fn.lower().endswith(want_ext):
                    continue
                if fn.lower() == base.lower() or os.path.splitext(fn)[0].lower() == base_noext:
                    return os.path.join(root, fn)
    return ""

def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool:
    """Gán preset lên plugin pedalboard: bytes nhúng (base64 .vstpreset) ưu
    tiên, sau đó preset_id (thư viện), sau preset_path (file). Trả True nếu
    áp dụng được; KHÔNG raise (render engine chỉ log warning)."""
    if plugin is None:
        return False
    try:
        if preset_data_b64:
            import base64
            raw = base64.b64decode(preset_data_b64)
            if hasattr(plugin, "preset_data"):
                plugin.preset_data = raw  # bytes dạng .vstpreset (VST3)
                return True
        p = resolve_preset_path(preset_id or "") or resolve_preset_path(preset_path or "")
        if p and hasattr(plugin, "load_preset"):
            plugin.load_preset(p)  # .vstpreset (VST3) / .dspreset (DecentSampler)
            return True
    except Exception:
        return False
    return False

