mod platform;
mod session;
mod subscriptions;
mod update;
mod view;

#[cfg(all(unix, not(target_os = "macos")))]
use crate::plugins::lv2::GuiLv2UiHost;
use crate::{
    add_track, clip_rename, config, connections,
    consts::audio_defaults,
    consts::gui as gui_consts,
    consts::gui_mod::{
        AUDIO_BIT_DEPTH_OPTIONS, BINS_PER_UPDATE, CHUNK_FRAMES, CLIENT, MAX_PEAK_BINS,
        MAX_RECENT_SESSIONS, STANDARD_EXPORT_SAMPLE_RATES,
    },
    consts::message_lists::{
        EXPORT_BIT_DEPTH_ALL, EXPORT_MP3_MODE_ALL, EXPORT_NORMALIZE_MODE_ALL,
        EXPORT_RENDER_MODE_ALL, SNAP_MODE_ALL,
    },
    consts::state_ids::METRONOME_TRACK_ID,
    consts::widget_piano::PITCH_MAX,
    hw, menu,
    message::{
        BurnBackendOption, DraggedClip, ExportBitDepth, ExportFormat, ExportMp3Mode,
        ExportNormalizeMode, ExportRenderMode, GenerateAudioModelOption, Message, PluginFormat,
        PreferencesDeviceOption, Show, SnapMode, TrackAutomationTarget,
    },
    platform_caps,
    plugins::{clap::GuiClapUiHost, vst3::GuiVst3UiHost},
    state::{
        AudioClip, ClipPeaks, LOG_HISTORY_LIMIT, LogEntry, LogLevel, MIDIClip, MidiClipPreviewMap,
        PianoControllerPoint, PianoNote, PianoSysExPoint, PitchCorrectionData,
        PitchCorrectionPoint, State, StateData,
    },
    template_save, toolbar, track_group, track_group_template_save, track_marker, track_rename,
    track_template_save, workspace,
};
use ebur128::{EbuR128, Mode as LoudnessMode};
use ffmpeg_next::{
    Dictionary,
    codec::{Context as CodecContext, Id as CodecId},
    format::output,
    frame::Audio,
};
use flacenc::component::BitRepr;
use flacenc::error::Verify;
use iced::{
    Color, Length, Size, Task,
    widget::{
        Column, button, checkbox, column, container, pick_list, progress_bar, row, scrollable,
        text, text_editor, text_input,
    },
};
use iced_aw::helpers::color_picker_with_change;
use maolan_engine::kind::Kind;
use maolan_engine::message::{
    Action, Message as EngineMessage, OfflineAutomationLane, OfflineAutomationPoint,
    OfflineAutomationTarget,
};
use maolan_widgets::numeric_input::{number_input, number_input_f32};
use midly::{
    Format, Header, MetaMessage, Smf, Timing, TrackEvent, TrackEventKind,
    num::{u15, u24, u28},
};
use pitch_detection::detector::{PitchDetector, mcleod::McLeodDetector};
use serde::Serialize;
use serde_json::Value;
#[allow(unused_imports)]
use serde_json::json;

use std::{
    collections::{BTreeSet, HashMap, HashSet},
    fs::{self, File},
    hash::{DefaultHasher, Hash, Hasher},
    io::{self, BufReader},
    path::{Path, PathBuf},
    process::Command,
    sync::atomic::AtomicBool,
    sync::{Arc, LazyLock, Mutex},
    time::{Duration, Instant},
};
use symphonia::core::{
    audio::SampleBuffer, codecs::DecoderOptions, errors::Error as SymphoniaError,
    formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint,
};
use tokio::sync::RwLock;
use tracing::error;

use hound::{SampleFormat, WavReader, WavSpec};

pub(crate) use gui_consts::{MIN_CLIP_WIDTH_PX, PREF_DEVICE_AUTO_ID};
type TickToSampleFn = dyn Fn(u64) -> usize + Send + Sync;
type MidiTickMap = (Box<TickToSampleFn>, u64, u64);

#[cfg(unix)]
const MAOLAN_BURN_SOCKETPAIR_ENV: &str = "MAOLAN_BURN_SOCKETPAIR";
#[derive(Debug, Clone, Serialize)]
struct BurnGenerateRequest {
    model: GenerateAudioModelOption,
    prompt: String,
    output_path: PathBuf,
    tags: Option<String>,
    backend: BurnBackendOption,
    cfg_scale: f32,
    ode_steps: usize,
    length: usize,
}

#[cfg(unix)]
struct BurnGenerateProcessHandle {
    socket: std::os::unix::net::UnixStream,
    stderr_log: Arc<Mutex<Vec<String>>>,
    exit_status: Arc<Mutex<Option<std::process::ExitStatus>>>,
}

type PianoParseResult = (
    Vec<PianoNote>,
    Vec<PianoControllerPoint>,
    Vec<PianoSysExPoint>,
    usize,
);
type TrackFreezeRestore = (Vec<AudioClip>, Vec<MIDIClip>, Option<String>);

pub(crate) const MIN_ZOOM_VISIBLE_BARS: f32 = 0.25;
pub(crate) const MAX_ZOOM_VISIBLE_BARS: f32 = 256.0;
pub(crate) static RUBBERBAND_AVAILABLE: LazyLock<bool> = LazyLock::new(|| {
    Command::new("rubberband")
        .arg("--version")
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
});

pub(crate) fn zoom_slider_to_visible_bars(position: f32) -> f32 {
    let clamped = position.clamp(0.0, 1.0);
    let min = MIN_ZOOM_VISIBLE_BARS.log2();
    let max = MAX_ZOOM_VISIBLE_BARS.log2();
    2.0_f32.powf(min + (max - min) * clamped)
}

pub(crate) fn visible_bars_to_zoom_slider(visible_bars: f32) -> f32 {
    let clamped = visible_bars.clamp(MIN_ZOOM_VISIBLE_BARS, MAX_ZOOM_VISIBLE_BARS);
    let min = MIN_ZOOM_VISIBLE_BARS.log2();
    let max = MAX_ZOOM_VISIBLE_BARS.log2();
    ((clamped.log2() - min) / (max - min)).clamp(0.0, 1.0)
}

#[derive(Debug, Clone)]
struct PendingTrackFreezeBounce {
    rendered_clip_rel: String,
    rendered_length: usize,
    backup_audio: Vec<AudioClip>,
    backup_midi: Vec<MIDIClip>,
}

#[derive(Debug, Clone)]
struct PendingAutosaveRecovery {
    session_dir: PathBuf,
    snapshots: Vec<PathBuf>,
    selected_index: usize,
    confirm_armed: bool,
}

#[derive(Debug, Clone)]
struct PitchCorrectionHistoryEntry {
    points: Vec<PitchCorrectionPoint>,
    selected_points: HashSet<usize>,
}

struct ExportSessionOptions {
    export_path: PathBuf,
    sample_rate: i32,
    formats: Vec<ExportFormat>,
    render_mode: ExportRenderMode,
    selected_hw_out_ports: Vec<usize>,
    realtime_fallback: bool,
    bit_depth: ExportBitDepth,
    mp3_mode: ExportMp3Mode,
    mp3_bitrate_kbps: u16,
    ogg_quality: f32,
    normalize: bool,
    normalize_target_dbfs: f32,
    normalize_mode: ExportNormalizeMode,
    normalize_target_lufs: f32,
    normalize_true_peak_dbtp: f32,
    normalize_tp_limiter: bool,
    master_limiter: bool,
    master_limiter_ceiling_dbtp: f32,
    metadata_author: String,
    metadata_album: String,
    metadata_year: Option<u32>,
    metadata_track_number: Option<u32>,
    metadata_genre: String,
    state: State,
    session_root: PathBuf,
}

#[derive(Clone)]
struct ExportMetadata {
    author: String,
    album: String,
    year: Option<u32>,
    track_number: Option<u32>,
    genre: String,
}

#[derive(Clone, Copy)]
struct ExportCodecSettings {
    mp3_mode: ExportMp3Mode,
    mp3_bitrate_kbps: u16,
    ogg_quality: f32,
}

struct ExportWriteRequest<'a> {
    export_path: &'a Path,
    mixed_buffer: &'a [f32],
    sample_rate: i32,
    output_channels: usize,
    bit_depth: ExportBitDepth,
    format: ExportFormat,
    codec: ExportCodecSettings,
    metadata: &'a ExportMetadata,
}

#[derive(Clone)]
struct ExportTrackData {
    name: String,
    level: f32,
    balance: f32,
    muted: bool,
    soloed: bool,
    output_ports: usize,
    clips: Vec<crate::state::AudioClip>,
}

fn build_offline_automation_lanes(
    lanes: &[crate::state::TrackAutomationLane],
) -> Vec<OfflineAutomationLane> {
    let mut automation_lanes = Vec::new();
    for lane in lanes.iter().filter(|lane| !lane.points.is_empty()) {
        let target = match lane.target {
            TrackAutomationTarget::Volume => OfflineAutomationTarget::Volume,
            TrackAutomationTarget::Balance => OfflineAutomationTarget::Balance,
            TrackAutomationTarget::Mute => OfflineAutomationTarget::Mute,
            #[cfg(all(unix, not(target_os = "macos")))]
            TrackAutomationTarget::Lv2Parameter {
                instance_id,
                index,
                min,
                max,
            } => OfflineAutomationTarget::Lv2Parameter {
                instance_id,
                index,
                min,
                max,
            },
            #[cfg(not(all(unix, not(target_os = "macos"))))]
            TrackAutomationTarget::Lv2Parameter { .. } => continue,
            TrackAutomationTarget::Vst3Parameter {
                instance_id,
                param_id,
            } => OfflineAutomationTarget::Vst3Parameter {
                instance_id,
                param_id,
            },
            TrackAutomationTarget::ClapParameter {
                instance_id,
                param_id,
                min,
                max,
            } => OfflineAutomationTarget::ClapParameter {
                instance_id,
                param_id,
                min,
                max,
            },
        };
        let points = lane
            .points
            .iter()
            .map(|p| OfflineAutomationPoint {
                sample: p.sample,
                value: p.value,
            })
            .collect::<Vec<_>>();
        automation_lanes.push(OfflineAutomationLane { target, points });
    }
    automation_lanes
}

#[derive(Debug, Clone)]
struct AppPreferences {
    osc_enabled: bool,
    default_export_sample_rate_hz: u32,
    default_snap_mode: SnapMode,
    default_midi_snap_mode: SnapMode,
    default_audio_bit_depth: usize,
    default_output_device_id: Option<String>,
    default_input_device_id: Option<String>,
    recent_session_paths: Vec<String>,
}

impl Default for AppPreferences {
    fn default() -> Self {
        Self {
            osc_enabled: false,
            default_export_sample_rate_hz: audio_defaults::SAMPLE_RATE_HZ as u32,
            default_snap_mode: SnapMode::Bar,
            default_midi_snap_mode: SnapMode::Bar,
            default_audio_bit_depth: audio_defaults::BIT_DEPTH,
            default_output_device_id: None,
            default_input_device_id: None,
            recent_session_paths: Vec::new(),
        }
    }
}

#[derive(Clone, Copy)]
struct ExportNormalizeParams {
    mode: ExportNormalizeMode,
    target_dbfs: f32,
    target_lufs: f32,
    true_peak_dbtp: f32,
    tp_limiter: bool,
    sample_rate: i32,
    output_channels: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct AudioClipKey {
    track_name: String,
    clip_name: String,
    start: usize,
    length: usize,
    offset: usize,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct SessionMediaCleanupReport {
    deleted_files: Vec<String>,
    failed_files: Vec<String>,
}

#[derive(Debug, Clone)]
pub(super) struct AudioPeakChunkUpdate {
    pub track_name: String,
    pub clip_name: String,
    pub start: usize,
    pub length: usize,
    pub offset: usize,
    pub channels: usize,
    pub target_bins: usize,
    pub bin_start: usize,
    pub peaks: Vec<Vec<[f32; 2]>>,
    pub done: bool,
}

pub(super) static AUDIO_PEAK_UPDATES: LazyLock<Mutex<Vec<AudioPeakChunkUpdate>>> =
    LazyLock::new(|| Mutex::new(Vec::new()));

#[derive(Debug, Clone)]
pub(super) struct PendingClapUiOpen {
    track_name: String,
    clip_idx: Option<usize>,
    instance_id: usize,
    plugin_path: String,
}

#[derive(Debug, Clone)]
pub(super) struct PendingVst3UiOpen {
    track_name: String,
    clip_idx: Option<usize>,
    instance_id: usize,
    plugin_path: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum AutomationWriteKey {
    Volume,
    Balance,
    Mute,
    Lv2 { instance_id: usize, index: u32 },
    Vst3 { instance_id: usize, param_id: u32 },
    Clap { instance_id: usize, param_id: u32 },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TimingSelectionLane {
    Tempo,
    TimeSignature,
}

#[derive(Debug, Clone)]
struct TouchAutomationOverride {
    value: f32,
    updated_at: Instant,
}

#[derive(Debug, Clone, Default)]
struct TrackAutomationRuntime {
    level_db: Option<f32>,
    balance: Option<f32>,
    muted: Option<bool>,
    #[cfg(all(unix, not(target_os = "macos")))]
    lv2_params: HashMap<(usize, u32), f32>,
    vst3_params: HashMap<(usize, u32), f32>,
    clap_params: HashMap<(usize, u32), f64>,
}

pub struct Maolan {
    clip: Option<DraggedClip>,
    clip_preview_target_track: Option<String>,
    clip_preview_target_valid: bool,
    clip_preview_snap_adjust_samples: f32,
    clip_snap_targets: Vec<crate::state::ClipId>,
    menu: menu::Menu,
    size: Size,
    state: State,
    toolbar: toolbar::Toolbar,
    track: Option<String>,
    workspace: workspace::Workspace,
    connections: connections::canvas_host::CanvasHost<connections::tracks::Graph>,
    #[cfg(all(unix, not(target_os = "macos")))]
    track_plugins: connections::canvas_host::CanvasHost<connections::plugins::Graph>,
    hw: hw::HW,
    modal: Option<Show>,
    add_track: add_track::AddTrackView,
    clip_rename: clip_rename::ClipRenameView,
    track_rename: track_rename::TrackRenameView,
    track_group: track_group::TrackGroupView,
    track_marker: track_marker::TrackMarkerView,
    track_template_save: track_template_save::TrackTemplateSaveView,
    track_group_template_save: track_group_template_save::TrackGroupTemplateSaveView,
    template_save: template_save::TemplateSaveView,
    #[cfg(all(unix, not(target_os = "macos")))]
    plugin_filter: String,
    #[cfg(all(unix, not(target_os = "macos")))]
    selected_lv2_plugins: BTreeSet<String>,
    vst3_plugin_filter: String,
    selected_vst3_plugins: BTreeSet<String>,
    clap_plugin_filter: String,
    selected_clap_plugins: BTreeSet<String>,
    plugin_format: PluginFormat,
    session_dir: Option<PathBuf>,
    pending_save_path: Option<String>,
    pending_save_tracks: std::collections::HashSet<String>,
    pending_save_clap_tracks: std::collections::HashSet<String>,
    #[cfg(target_os = "macos")]
    pending_save_vst3_states: HashSet<(String, usize)>,
    pending_save_is_template: bool,
    pending_peak_file_loads: HashMap<AudioClipKey, PathBuf>,
    pending_peak_rebuilds: HashSet<AudioClipKey>,
    pending_precomputed_peaks: HashMap<AudioClipKey, crate::state::ClipPeaks>,
    pending_source_lengths: HashMap<AudioClipKey, usize>,
    undo_peaks_cache: HashMap<AudioClipKey, crate::state::ClipPeaks>,
    undo_source_lengths_cache: HashMap<AudioClipKey, usize>,
    pitch_correction_undo: Vec<PitchCorrectionHistoryEntry>,
    pitch_correction_redo: Vec<PitchCorrectionHistoryEntry>,
    pending_track_freeze_restore: HashMap<String, TrackFreezeRestore>,
    pending_track_midi_editor_view_mode: HashMap<String, crate::message::MidiEditorViewMode>,
    pending_track_freeze_bounce: HashMap<String, PendingTrackFreezeBounce>,
    track_automation_runtime: HashMap<String, TrackAutomationRuntime>,
    touch_automation_overrides:
        HashMap<String, HashMap<AutomationWriteKey, TouchAutomationOverride>>,
    touch_active_keys: HashMap<String, HashSet<AutomationWriteKey>>,
    latch_automation_overrides: HashMap<String, HashMap<AutomationWriteKey, f32>>,
    #[cfg(all(unix, not(target_os = "macos")))]
    pending_add_lv2_automation_uris: HashSet<(String, String)>,
    #[cfg(all(unix, not(target_os = "macos")))]
    pending_add_lv2_automation_instances: HashSet<(String, usize)>,
    pending_add_vst3_automation_paths: HashSet<(String, String)>,
    pending_add_vst3_automation_instances: HashSet<(String, usize)>,
    pending_add_clap_automation_paths: HashSet<(String, String)>,
    pending_add_clap_automation_instances: HashSet<(String, usize)>,
    midi_clip_previews: MidiClipPreviewMap,
    pending_midi_clip_previews: HashSet<(String, usize, String)>,
    freeze_in_progress: bool,
    freeze_progress: f32,
    freeze_track_name: Option<String>,
    freeze_cancel_requested: bool,
    playing: bool,
    paused: bool,
    metronome_enabled: bool,
    transport_samples: f64,
    last_playback_tick: Option<Instant>,
    playback_rate_hz: f64,
    loop_enabled: bool,
    loop_range_samples: Option<(usize, usize)>,
    punch_enabled: bool,
    punch_range_samples: Option<(usize, usize)>,
    snap_mode: SnapMode,
    midi_snap_mode: SnapMode,
    zoom_visible_bars: f32,
    editor_scroll_origin_samples: f64,
    editor_scroll_x: f32,
    editor_scroll_y: f32,
    mixer_scroll_x: f32,
    tracks_resize_hovered: bool,
    mixer_resize_hovered: bool,
    tracks_visible: bool,
    editor_visible: bool,
    mixer_visible: bool,
    show_log_window: bool,
    shortcuts_pane_visible: bool,
    hw_mixer: mixosc::app::StatusApp,
    mixer_level_edit_track: Option<String>,
    mixer_level_edit_input: String,
    record_armed: bool,
    pending_record_after_save: bool,
    recording_preview_start_sample: Option<usize>,
    recording_preview_sample: Option<usize>,
    recording_preview_peaks: HashMap<String, ClipPeaks>,
    import_in_progress: bool,
    import_current_file: usize,
    import_total_files: usize,
    import_file_progress: f32,
    import_current_filename: String,
    import_current_operation: Option<String>,
    generate_audio_model: GenerateAudioModelOption,
    generate_audio_prompt_editor: text_editor::Content,
    log_viewer_content: text_editor::Content,

    generate_audio_tags_input: String,
    generate_audio_backend: BurnBackendOption,

    generate_audio_cfg_scale_input: String,
    generate_audio_steps_input: usize,
    generate_audio_seconds_total_input: usize,
    generate_audio_in_progress: bool,
    generate_audio_progress: f32,
    generate_audio_operation: Option<String>,
    generate_audio_abort_handle: Option<tokio::task::AbortHandle>,
    #[cfg(unix)]
    generate_audio_process_id: Option<u32>,
    clip_pitch_correction_in_progress: bool,
    clip_pitch_correction_progress: f32,
    clip_pitch_correction_clip_name: String,
    clip_pitch_correction_operation: Option<String>,
    export_in_progress: bool,
    export_cancel: Arc<AtomicBool>,
    export_pending_bounces: HashSet<String>,
    export_bounce_notify: Option<Arc<tokio::sync::Notify>>,
    export_progress: f32,
    export_operation: Option<String>,
    export_sample_rate_hz: u32,
    export_format_wav: bool,
    export_format_mp3: bool,
    export_format_ogg: bool,
    export_format_flac: bool,
    export_bit_depth: ExportBitDepth,
    export_mp3_mode: ExportMp3Mode,
    export_mp3_bitrate_kbps: u16,
    export_ogg_quality_input: String,
    export_render_mode: ExportRenderMode,
    export_hw_out_ports: BTreeSet<usize>,
    export_realtime_fallback: bool,
    export_normalize: bool,
    export_normalize_mode: ExportNormalizeMode,
    export_normalize_dbfs_input: String,
    export_normalize_lufs_input: String,
    export_normalize_dbtp_input: String,
    export_normalize_tp_limiter: bool,
    export_master_limiter: bool,
    export_master_limiter_ceiling_input: String,
    clap_param_values: HashMap<(String, Option<usize>, usize, u32), f64>,
    clap_ui_host: GuiClapUiHost,
    #[cfg(all(unix, not(target_os = "macos")))]
    lv2_ui_host: GuiLv2UiHost,
    vst3_ui_host: GuiVst3UiHost,
    pending_clap_ui_open: Option<PendingClapUiOpen>,
    pending_vst3_ui_open: Option<PendingVst3UiOpen>,
    tempo_input: String,
    time_signature_num_input: String,
    time_signature_denom_input: String,
    tap_tempo_times: Vec<Instant>,
    last_sent_tempo_bpm: Option<f64>,
    last_sent_time_signature: Option<(u16, u16)>,
    selected_tempo_points: BTreeSet<usize>,
    selected_time_signature_points: BTreeSet<usize>,
    timing_selection_lane: Option<TimingSelectionLane>,
    midi_mappings_panel_open: bool,
    midi_mappings_report_lines: Vec<String>,
    has_unsaved_changes: bool,
    engine_dirty: bool,
    pitch_correction_dirty: bool,
    pending_exit_after_save: bool,
    session_restore_in_progress: bool,
    last_autosave_snapshot: Option<Instant>,
    pending_recovery_session_dir: Option<PathBuf>,
    pending_autosave_recovery: Option<PendingAutosaveRecovery>,
    pending_open_session_dir: Option<PathBuf>,
    pending_diagnostics_bundle_export: bool,
    diagnostics_bundle_wait_session_report: bool,
    diagnostics_bundle_wait_midi_report: bool,
    prefs_osc_enabled: bool,
    prefs_export_sample_rate_hz: u32,
    prefs_snap_mode: SnapMode,
    prefs_midi_snap_mode: SnapMode,
    prefs_audio_bit_depth: usize,
    prefs_default_output_device_id: Option<String>,
    prefs_default_input_device_id: Option<String>,
}

fn load_preferences() -> AppPreferences {
    let cfg = config::Config::load().unwrap_or_default();
    AppPreferences {
        osc_enabled: cfg.osc_enabled,
        default_export_sample_rate_hz: cfg.default_export_sample_rate_hz,
        default_snap_mode: cfg.default_snap_mode,
        default_midi_snap_mode: cfg.default_midi_snap_mode,
        default_audio_bit_depth: cfg.default_audio_bit_depth,
        default_output_device_id: cfg.default_output_device_id,
        default_input_device_id: cfg.default_input_device_id,
        recent_session_paths: cfg.recent_session_paths,
    }
}

fn scan_templates() -> Vec<String> {
    let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
    let templates_dir = format!("{}/.config/maolan/session_templates", home);

    let Ok(entries) = std::fs::read_dir(&templates_dir) else {
        return vec![];
    };

    entries
        .filter_map(|entry| {
            let entry = entry.ok()?;
            let path = entry.path();
            if path.is_dir() {
                path.file_name()?.to_str().map(|s| s.to_string())
            } else {
                None
            }
        })
        .collect()
}

fn scan_track_templates() -> Vec<String> {
    let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
    let templates_dir = format!("{}/.config/maolan/track_templates", home);

    let Ok(entries) = std::fs::read_dir(&templates_dir) else {
        return vec![];
    };

    entries
        .filter_map(|entry| {
            let entry = entry.ok()?;
            let path = entry.path();
            if path.is_dir() {
                path.file_name()?.to_str().map(|s| s.to_string())
            } else {
                None
            }
        })
        .collect()
}

fn scan_group_templates() -> Vec<String> {
    let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
    let templates_dir = format!("{}/.config/maolan/group_templates", home);

    let Ok(entries) = std::fs::read_dir(&templates_dir) else {
        return vec![];
    };

    entries
        .filter_map(|entry| {
            let entry = entry.ok()?;
            let path = entry.path();
            if path.is_dir() {
                path.file_name()?.to_str().map(|s| s.to_string())
            } else {
                None
            }
        })
        .collect()
}

impl Default for Maolan {
    fn default() -> Self {
        let prefs = load_preferences();
        let mut state_data = StateData {
            available_templates: scan_templates(),
            ..StateData::default()
        };
        Self::apply_preferred_devices_to_state(&mut state_data, &prefs);
        let state = Arc::new(RwLock::new(state_data));
        if tokio::runtime::Handle::try_current().is_ok() {
            let _ = CLIENT
                .sender
                .blocking_send(EngineMessage::Request(Action::SetOscEnabled(
                    prefs.osc_enabled,
                )));
        }
        let mut menu = menu::Menu::default();
        menu.update_templates(scan_templates());
        menu.update_recent_sessions(Self::normalize_recent_session_paths(
            prefs.recent_session_paths.clone(),
        ));
        Self {
            clip: None,
            clip_preview_target_track: None,
            clip_preview_target_valid: false,
            clip_preview_snap_adjust_samples: 0.0,
            clip_snap_targets: Vec::new(),
            menu,
            size: Size::new(0.0, 0.0),
            state: state.clone(),
            toolbar: toolbar::Toolbar::new(),
            track: None,
            workspace: workspace::Workspace::new(state.clone()),
            connections: connections::canvas_host::CanvasHost::new(
                connections::tracks::Graph::new(state.clone()),
            ),
            #[cfg(all(unix, not(target_os = "macos")))]
            track_plugins: connections::canvas_host::CanvasHost::new(
                connections::plugins::Graph::new(state.clone()),
            ),
            hw: hw::HW::new(state.clone()),
            modal: None,
            add_track: add_track::AddTrackView::default(),
            clip_rename: clip_rename::ClipRenameView::new(state.clone()),
            track_rename: track_rename::TrackRenameView::new(state.clone()),
            track_group: track_group::TrackGroupView::new(state.clone()),
            track_marker: track_marker::TrackMarkerView::new(state.clone()),
            track_template_save: track_template_save::TrackTemplateSaveView::new(state.clone()),
            track_group_template_save: track_group_template_save::TrackGroupTemplateSaveView::new(
                state.clone(),
            ),
            template_save: template_save::TemplateSaveView::new(state.clone()),
            #[cfg(all(unix, not(target_os = "macos")))]
            plugin_filter: String::new(),
            #[cfg(all(unix, not(target_os = "macos")))]
            selected_lv2_plugins: BTreeSet::new(),
            vst3_plugin_filter: String::new(),
            selected_vst3_plugins: BTreeSet::new(),
            clap_plugin_filter: String::new(),
            selected_clap_plugins: BTreeSet::new(),
            plugin_format: Self::default_plugin_format(),
            session_dir: None,
            pending_save_path: None,
            pending_save_tracks: std::collections::HashSet::new(),
            pending_save_clap_tracks: std::collections::HashSet::new(),
            #[cfg(target_os = "macos")]
            pending_save_vst3_states: HashSet::new(),
            pending_save_is_template: false,
            pending_peak_file_loads: HashMap::new(),
            pending_peak_rebuilds: HashSet::new(),
            pending_precomputed_peaks: HashMap::new(),
            pending_source_lengths: HashMap::new(),
            undo_peaks_cache: HashMap::new(),
            undo_source_lengths_cache: HashMap::new(),
            pitch_correction_undo: Vec::new(),
            pitch_correction_redo: Vec::new(),
            pending_track_freeze_restore: HashMap::new(),
            pending_track_midi_editor_view_mode: HashMap::new(),
            pending_track_freeze_bounce: HashMap::new(),
            track_automation_runtime: HashMap::new(),
            touch_automation_overrides: HashMap::new(),
            touch_active_keys: HashMap::new(),
            latch_automation_overrides: HashMap::new(),
            #[cfg(all(unix, not(target_os = "macos")))]
            pending_add_lv2_automation_uris: HashSet::new(),
            #[cfg(all(unix, not(target_os = "macos")))]
            pending_add_lv2_automation_instances: HashSet::new(),
            pending_add_vst3_automation_paths: HashSet::new(),
            pending_add_vst3_automation_instances: HashSet::new(),
            pending_add_clap_automation_paths: HashSet::new(),
            pending_add_clap_automation_instances: HashSet::new(),
            midi_clip_previews: HashMap::new(),
            pending_midi_clip_previews: HashSet::new(),
            freeze_in_progress: false,
            freeze_progress: 0.0,
            freeze_track_name: None,
            freeze_cancel_requested: false,
            playing: false,
            paused: false,
            metronome_enabled: false,
            transport_samples: 0.0,
            last_playback_tick: None,
            playback_rate_hz: 48_000.0,
            loop_enabled: false,
            loop_range_samples: None,
            punch_enabled: false,
            punch_range_samples: None,
            snap_mode: prefs.default_snap_mode,
            midi_snap_mode: prefs.default_midi_snap_mode,
            zoom_visible_bars: 127.0,
            editor_scroll_origin_samples: 0.0,
            editor_scroll_x: 0.0,
            editor_scroll_y: 0.0,
            mixer_scroll_x: 0.0,
            tracks_resize_hovered: false,
            mixer_resize_hovered: false,
            tracks_visible: true,
            editor_visible: true,
            mixer_visible: true,
            show_log_window: false,
            shortcuts_pane_visible: false,
            hw_mixer: mixosc::app::StatusApp::default(),
            mixer_level_edit_track: None,
            mixer_level_edit_input: String::new(),
            record_armed: false,
            pending_record_after_save: false,
            recording_preview_start_sample: None,
            recording_preview_sample: None,
            recording_preview_peaks: HashMap::new(),
            import_in_progress: false,
            import_current_file: 0,
            import_total_files: 0,
            import_file_progress: 0.0,
            import_current_filename: String::new(),
            import_current_operation: None,
            generate_audio_model: GenerateAudioModelOption::HappyNewYear,
            generate_audio_prompt_editor: text_editor::Content::new(),
            log_viewer_content: text_editor::Content::with_text(
                "[INFO] Thank you for using Maolan!",
            ),

            generate_audio_tags_input: String::new(),
            generate_audio_backend: BurnBackendOption::Vulkan,

            generate_audio_cfg_scale_input: maolan_generate::DEFAULT_CFG_SCALE.to_string(),
            generate_audio_steps_input: 10,
            generate_audio_seconds_total_input: 180_usize,
            generate_audio_in_progress: false,
            generate_audio_progress: 0.0,
            generate_audio_operation: None,
            generate_audio_abort_handle: None,
            #[cfg(unix)]
            generate_audio_process_id: None,
            clip_pitch_correction_in_progress: false,
            clip_pitch_correction_progress: 0.0,
            clip_pitch_correction_clip_name: String::new(),
            clip_pitch_correction_operation: None,
            export_in_progress: false,
            export_cancel: Arc::new(AtomicBool::new(false)),
            export_pending_bounces: HashSet::new(),
            export_bounce_notify: None,
            export_progress: 0.0,
            export_operation: None,
            export_sample_rate_hz: prefs.default_export_sample_rate_hz,
            export_format_wav: true,
            export_format_mp3: false,
            export_format_ogg: false,
            export_format_flac: false,
            export_bit_depth: ExportBitDepth::Int24,
            export_mp3_mode: ExportMp3Mode::Cbr,
            export_mp3_bitrate_kbps: 320,
            export_ogg_quality_input: "0.6".to_string(),
            export_render_mode: ExportRenderMode::Mixdown,
            export_hw_out_ports: [0_usize, 1].into_iter().collect(),
            export_realtime_fallback: false,
            export_normalize: false,
            export_normalize_mode: ExportNormalizeMode::Peak,
            export_normalize_dbfs_input: "0.0".to_string(),
            export_normalize_lufs_input: "-23.0".to_string(),
            export_normalize_dbtp_input: "-1.0".to_string(),
            export_normalize_tp_limiter: true,
            export_master_limiter: true,
            export_master_limiter_ceiling_input: "-1.0".to_string(),
            clap_param_values: HashMap::new(),
            clap_ui_host: GuiClapUiHost::new(),
            #[cfg(all(unix, not(target_os = "macos")))]
            lv2_ui_host: GuiLv2UiHost::new(),
            vst3_ui_host: GuiVst3UiHost::new(),
            pending_clap_ui_open: None,
            pending_vst3_ui_open: None,
            tempo_input: "120".to_string(),
            time_signature_num_input: "4".to_string(),
            time_signature_denom_input: "4".to_string(),
            tap_tempo_times: Vec::new(),
            last_sent_tempo_bpm: Some(120.0),
            last_sent_time_signature: Some((4, 4)),
            selected_tempo_points: BTreeSet::new(),
            selected_time_signature_points: BTreeSet::new(),
            timing_selection_lane: None,
            midi_mappings_panel_open: false,
            midi_mappings_report_lines: Vec::new(),
            has_unsaved_changes: false,
            engine_dirty: false,
            pitch_correction_dirty: false,
            pending_exit_after_save: false,
            session_restore_in_progress: false,
            last_autosave_snapshot: None,
            pending_recovery_session_dir: None,
            pending_autosave_recovery: None,
            pending_open_session_dir: None,
            pending_diagnostics_bundle_export: false,
            diagnostics_bundle_wait_session_report: false,
            diagnostics_bundle_wait_midi_report: false,
            prefs_osc_enabled: prefs.osc_enabled,
            prefs_export_sample_rate_hz: prefs.default_export_sample_rate_hz,
            prefs_snap_mode: prefs.default_snap_mode,
            prefs_midi_snap_mode: prefs.default_midi_snap_mode,
            prefs_audio_bit_depth: prefs.default_audio_bit_depth,
            prefs_default_output_device_id: prefs.default_output_device_id,
            prefs_default_input_device_id: prefs.default_input_device_id,
        }
    }
}

impl Maolan {
    fn is_dirty(&self) -> bool {
        self.has_unsaved_changes || self.engine_dirty || self.pitch_correction_dirty
    }

    fn push_log_entry(state: &mut StateData, level: LogLevel, message: String) {
        state.message = message.clone();
        state.log_entries.push(LogEntry { level, message });
        if state.log_entries.len() > LOG_HISTORY_LIMIT {
            let drop_count = state.log_entries.len() - LOG_HISTORY_LIMIT;
            state.log_entries.drain(0..drop_count);
        }
    }

    fn refresh_log_viewer_content(&mut self) {
        let log_text = self
            .state
            .blocking_read()
            .log_entries
            .iter()
            .map(|entry| format!("[{}] {}", entry.level, entry.message))
            .collect::<Vec<_>>()
            .join("\n");
        self.log_viewer_content = text_editor::Content::with_text(&log_text);
    }

    fn sync_message_log_from_state(&mut self) {
        let mut state = self.state.blocking_write();
        let needs_append = state
            .log_entries
            .last()
            .map(|entry| entry.message.as_str() != state.message.as_str())
            .unwrap_or(true);
        if needs_append {
            let message = state.message.clone();
            Self::push_log_entry(&mut state, LogLevel::Info, message);
            drop(state);
            self.refresh_log_viewer_content();
        }
    }

    fn info(&mut self, message: impl Into<String>) {
        let message = message.into();
        tracing::info!("{message}");
        let mut state = self.state.blocking_write();
        Self::push_log_entry(&mut state, LogLevel::Info, message);
        drop(state);
        self.refresh_log_viewer_content();
    }

    fn warning(&mut self, message: impl Into<String>) {
        let message = message.into();
        tracing::warn!("{message}");
        let mut state = self.state.blocking_write();
        Self::push_log_entry(&mut state, LogLevel::Warning, message);
        drop(state);
        self.refresh_log_viewer_content();
    }

    fn error(&mut self, message: impl Into<String>) {
        let message = message.into();
        tracing::error!("{message}");
        let mut state = self.state.blocking_write();
        Self::push_log_entry(&mut state, LogLevel::Error, message);
        drop(state);
        self.refresh_log_viewer_content();
    }

    #[cfg(unix)]
    fn maolan_workspace_root() -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
    }

    #[cfg(unix)]
    fn resolve_maolan_burn_binary_path(
        current_exe: Option<&Path>,
        _workspace_root: &Path,
    ) -> Option<PathBuf> {
        let mut candidates = Vec::new();
        if let Some(current_exe) = current_exe
            && let Some(parent) = current_exe.parent()
        {
            candidates.push(parent.join("maolan-generate"));
        }
        candidates.into_iter().find(|path| path.exists())
    }

    #[cfg(unix)]
    fn maolan_burn_command() -> Command {
        let workspace_root = Self::maolan_workspace_root();

        if let Some(path) = Self::resolve_maolan_burn_binary_path(
            std::env::current_exe().ok().as_deref(),
            &workspace_root,
        ) {
            return Command::new(path);
        }
        Command::new("maolan-generate")
    }

    #[cfg(unix)]
    fn write_ipc_message<T: Serialize>(
        writer: &mut impl std::io::Write,
        value: &T,
    ) -> Result<(), String> {
        let payload =
            serde_json::to_vec(value).map_err(|e| format!("Failed to encode IPC request: {e}"))?;
        let len = u64::try_from(payload.len()).map_err(|_| "IPC payload too large".to_string())?;
        writer
            .write_all(&len.to_le_bytes())
            .map_err(|e| format!("Failed to write IPC request length: {e}"))?;
        writer
            .write_all(&payload)
            .map_err(|e| format!("Failed to write IPC request payload: {e}"))?;
        writer
            .flush()
            .map_err(|e| format!("Failed to flush IPC request: {e}"))?;
        Ok(())
    }

    #[cfg(unix)]
    fn read_ipc_payload(reader: &mut impl std::io::Read) -> Result<Vec<u8>, String> {
        let mut len_bytes = [0_u8; 8];
        reader
            .read_exact(&mut len_bytes)
            .map_err(|e| format!("Failed to read IPC response length: {e}"))?;
        let len = usize::try_from(u64::from_le_bytes(len_bytes))
            .map_err(|_| "IPC response too large".to_string())?;
        let mut payload = vec![0_u8; len];
        reader
            .read_exact(&mut payload)
            .map_err(|e| format!("Failed to read IPC response payload: {e}"))?;
        Ok(payload)
    }

    #[cfg(unix)]
    fn spawn_generate_process(
        request: &BurnGenerateRequest,
    ) -> Result<(u32, BurnGenerateProcessHandle), String> {
        use std::io::{BufRead, BufReader};
        use std::os::fd::OwnedFd;
        use std::os::unix::net::UnixStream;
        use std::process::Stdio;

        let (mut parent, child) =
            UnixStream::pair().map_err(|e| format!("Failed to create socketpair: {e}"))?;

        let child_read = child
            .try_clone()
            .map_err(|e| format!("Failed to clone child socket: {e}"))?;
        let mut command = Self::maolan_burn_command();
        command.env(MAOLAN_BURN_SOCKETPAIR_ENV, "1");
        let mut process = command
            .stdin(Stdio::from(OwnedFd::from(child_read)))
            .stdout(Stdio::from(OwnedFd::from(child)))
            .stderr(Stdio::piped())
            .spawn()
            .map_err(|e| format!("Failed to launch generate: {e}"))?;
        let pid = process.id();

        let stderr_log = Arc::new(Mutex::new(Vec::<String>::new()));
        let exit_status = Arc::new(Mutex::new(None));

        let stderr_log_bg = stderr_log.clone();
        let exit_status_bg = exit_status.clone();
        std::thread::spawn(move || {
            if let Some(stderr) = process.stderr.take() {
                let reader = BufReader::new(stderr);
                for line in reader.lines() {
                    match line {
                        Ok(line) => {
                            error!("maolan-generate: {line}");
                            if let Ok(mut log) = stderr_log_bg.lock() {
                                log.push(line);
                                if log.len() > 64 {
                                    let drop_count = log.len() - 64;
                                    log.drain(0..drop_count);
                                }
                            }
                        }
                        Err(err) => {
                            error!("Failed to read maolan-generate stderr: {err}");
                            break;
                        }
                    }
                }
            }
            let status = process.wait().ok();
            if let Ok(mut slot) = exit_status_bg.lock() {
                *slot = status;
            }
        });

        Self::write_ipc_message(&mut parent, request)?;

        Ok((
            pid,
            BurnGenerateProcessHandle {
                socket: parent,
                stderr_log,
                exit_status,
            },
        ))
    }

    #[cfg(unix)]
    fn communicate_with_generate_process<F>(
        mut process: BurnGenerateProcessHandle,
        mut progress_callback: F,
    ) -> Result<(), String>
    where
        F: FnMut(&str, f32, &str),
    {
        use maolan_generate::GenerateError;
        use maolan_generate::GenerateProgress;
        use maolan_generate::GenerateResponseHeader;

        let header: GenerateResponseHeader;
        loop {
            let payload = match Self::read_ipc_payload(&mut process.socket) {
                Ok(payload) => payload,
                Err(err) => {
                    let stderr_tail = process
                        .stderr_log
                        .lock()
                        .ok()
                        .map(|log| log.join("\n"))
                        .unwrap_or_default();
                    let exit_status = process
                        .exit_status
                        .lock()
                        .ok()
                        .and_then(|status| *status)
                        .map(|status| status.to_string())
                        .unwrap_or_else(|| "unknown".to_string());
                    let stderr_suffix = if stderr_tail.trim().is_empty() {
                        String::new()
                    } else {
                        format!("\nChild stderr:\n{stderr_tail}")
                    };
                    return Err(format!(
                        "{err}\nmaolan-generate exited before completing IPC (status: {exit_status}){stderr_suffix}"
                    ));
                }
            };

            match serde_json::from_slice::<GenerateProgress>(&payload) {
                Ok(progress) => {
                    progress_callback(&progress.phase, progress.progress, &progress.operation);
                    continue;
                }
                Err(_) => {
                    if let Ok(error) = serde_json::from_slice::<GenerateError>(&payload) {
                        return Err(error.error);
                    }
                    header = match serde_json::from_slice::<GenerateResponseHeader>(&payload) {
                        Ok(h) => h,
                        Err(err) => {
                            return Err(format!("Failed to decode IPC response payload: {err}"));
                        }
                    };
                    break;
                }
            }
        }

        drop(process.socket);
        let _ = header;
        Ok(())
    }

    #[cfg(not(unix))]
    fn spawn_generate_process(_request: &BurnGenerateRequest) -> Result<(u32, ()), String> {
        Err("Generated audio via generate is only available on Unix platforms".to_string())
    }

    #[cfg(not(unix))]
    fn communicate_with_generate_process<F>(
        _socket: (),
        _progress_callback: F,
    ) -> Result<(), String>
    where
        F: FnMut(&str, f32, &str),
    {
        Err("Generated audio via generate is only available on Unix platforms".to_string())
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    fn plugin_graph_title(state: &StateData) -> String {
        if let Some(target) = state.plugin_graph_clip.as_ref() {
            if let Some(track) = state
                .tracks
                .iter()
                .find(|track| track.name == target.track_name)
                && let Some(clip) = track.audio.clips.get(target.clip_idx)
            {
                return format!("Clip Plugins: {} / {}", target.track_name, clip.name);
            }
            return format!(
                "Clip Plugins: {} / clip {}",
                target.track_name, target.clip_idx
            );
        }
        format!(
            "Track Plugins: {}",
            state
                .plugin_graph_track
                .clone()
                .unwrap_or_else(|| "(no track)".to_string())
        )
    }

    fn normalize_recent_session_paths(paths: Vec<String>) -> Vec<String> {
        let mut normalized = Vec::new();
        let mut seen = HashSet::new();
        for path in paths {
            let trimmed = path.trim();
            if trimmed.is_empty() {
                continue;
            }
            if seen.insert(trimmed.to_string()) {
                normalized.push(trimmed.to_string());
            }
            if normalized.len() >= MAX_RECENT_SESSIONS {
                break;
            }
        }
        normalized
    }

    fn remember_recent_session_path(&mut self, path: &Path) {
        let current = path.to_string_lossy().to_string();
        if current.trim().is_empty() {
            return;
        }
        let mut cfg = config::Config::load().unwrap_or_default();
        let mut recent = vec![current.clone()];
        recent.extend(
            cfg.recent_session_paths
                .into_iter()
                .filter(|p| p != &current),
        );
        let recent = Self::normalize_recent_session_paths(recent);
        cfg.recent_session_paths = recent.clone();
        if let Err(err) = cfg.save() {
            error!("Failed to save recent session paths: {err}");
        }
        self.menu.update_recent_sessions(recent);
    }

    fn forget_recent_session_path(&mut self, path: &Path) {
        let target = path.to_string_lossy().to_string();
        if target.trim().is_empty() {
            return;
        }
        let mut cfg = config::Config::load().unwrap_or_default();
        let recent: Vec<String> = cfg
            .recent_session_paths
            .into_iter()
            .filter(|p| p != &target)
            .collect();
        let recent = Self::normalize_recent_session_paths(recent);
        cfg.recent_session_paths = recent.clone();
        if let Err(err) = cfg.save() {
            error!("Failed to save recent session paths: {err}");
        }
        self.menu.update_recent_sessions(recent);
    }

    fn default_plugin_format() -> PluginFormat {
        PluginFormat::Clap
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    fn supported_plugin_formats() -> Vec<PluginFormat> {
        let mut formats = Vec::new();
        formats.push(PluginFormat::Clap);
        if platform_caps::SUPPORTS_LV2 {
            formats.push(PluginFormat::Lv2);
        }
        formats.push(PluginFormat::Vst3);
        formats
    }

    fn session_display_name_from_path(path: &Path) -> Option<String> {
        let mut display_path = path;
        if let Some(parent) = path.parent()
            && parent
                .file_name()
                .and_then(|n| n.to_str())
                .is_some_and(|n| n == "snapshots")
            && parent
                .parent()
                .and_then(|p| p.file_name())
                .and_then(|n| n.to_str())
                .is_some_and(|n| n == ".maolan_autosave")
            && let Some(session_root) = parent.parent().and_then(Path::parent)
        {
            display_path = session_root;
        }
        display_path
            .file_name()
            .map(|name| name.to_string_lossy().to_string())
            .filter(|name| !name.is_empty())
    }

    pub fn title(&self) -> String {
        let session = self
            .session_dir
            .as_ref()
            .and_then(|path| Self::session_display_name_from_path(path))
            .unwrap_or_else(|| "<New>".to_string());
        let dirty_suffix = if self.is_dirty() { " *" } else { "" };
        format!("Maolan: {session}{dirty_suffix}")
    }

    fn samples_per_beat(&self) -> f64 {
        let (tempo, denom) = {
            let state = self.state.blocking_read();
            (
                state.tempo.max(1.0) as f64,
                state.time_signature_denom.max(1) as f64,
            )
        };
        let quarter = self.playback_rate_hz * 60.0 / tempo;
        quarter * (4.0 / denom)
    }

    fn samples_per_bar(&self) -> f64 {
        let beats_per_bar = self.state.blocking_read().time_signature_num.max(1) as f64;
        self.samples_per_beat() * beats_per_bar
    }

    fn snap_sample_to_bar(&self, sample: f32) -> usize {
        self.snap_mode.snap_sample(
            sample as f64,
            self.samples_per_beat(),
            self.samples_per_bar(),
        ) as usize
    }

    fn snap_sample_to_bar_drag(&self, sample: f32, delta_samples: f32) -> usize {
        self.snap_mode.snap_sample_drag(
            sample as f64,
            delta_samples as f64,
            self.samples_per_beat(),
            self.samples_per_bar(),
        ) as usize
    }

    fn snap_interval_samples(&self) -> usize {
        self.snap_mode
            .interval_samples(self.samples_per_beat(), self.samples_per_bar()) as usize
    }

    fn create_empty_midi_clip_file(
        &self,
        track_name: &str,
        session_root: &Path,
    ) -> std::io::Result<String> {
        fs::create_dir_all(session_root.join("midi"))?;
        let stem = format!("{}_clip", Self::sanitize_peak_file_component(track_name));
        let rel = Self::unique_import_rel_path(session_root, "midi", &stem, "mid")?;
        let path = session_root.join(&rel);
        let events = vec![
            TrackEvent {
                delta: u28::new(0),
                kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::new(500_000))),
            },
            TrackEvent {
                delta: u28::new(0),
                kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
            },
        ];
        let smf = Smf {
            header: Header::new(Format::SingleTrack, Timing::Metrical(u15::new(480))),
            tracks: vec![events],
        };
        let mut file = File::create(&path)?;
        smf.write_std(&mut file)
            .map_err(|e| io::Error::other(format!("Failed to write '{}': {e}", path.display())))?;
        Ok(rel)
    }

    fn tracks_width_px(&self) -> f32 {
        match self.state.blocking_read().tracks_width {
            Length::Fixed(v) => v,
            _ => 200.0,
        }
    }

    fn editor_width_px(&self) -> f32 {
        (self.size.width - self.tracks_width_px() - 3.0).max(1.0)
    }

    fn pixels_per_sample(&self) -> f32 {
        let total_samples = self.samples_per_bar() * self.zoom_visible_bars as f64;
        if total_samples <= 0.0 {
            return 1.0;
        }
        (self.editor_width_px() as f64 / total_samples) as f32
    }

    fn editor_visible_samples(&self) -> f64 {
        (self.samples_per_bar() * self.zoom_visible_bars as f64).max(1.0)
    }

    fn editor_timeline_samples(&self) -> f64 {
        let state = self.state.blocking_read();
        let max_end_samples = state
            .tracks
            .iter()
            .filter(|track| track.name != METRONOME_TRACK_ID)
            .flat_map(|track| {
                let audio = track
                    .audio
                    .clips
                    .iter()
                    .map(|clip| clip.start.saturating_add(clip.length));
                let midi = track
                    .midi
                    .clips
                    .iter()
                    .map(|clip| clip.start.saturating_add(clip.length));
                audio.chain(midi)
            })
            .max()
            .unwrap_or(0) as f64;
        let visible_samples = self.editor_visible_samples();
        let right_padding_samples = visible_samples * 0.5;
        let min_timeline_samples =
            (self.samples_per_bar() * crate::consts::workspace::MIN_TIMELINE_BARS as f64).max(1.0);
        max_end_samples
            .max(self.transport_samples.max(0.0) + right_padding_samples)
            .max(max_end_samples + right_padding_samples)
            .max(visible_samples)
            .max(min_timeline_samples)
    }

    fn editor_max_scroll_samples(&self) -> f64 {
        (self.editor_timeline_samples() - self.editor_visible_samples()).max(0.0)
    }

    fn editor_scroll_relative_x(&self) -> f32 {
        let max_scroll = self.editor_max_scroll_samples();
        if max_scroll <= 0.0 {
            0.0
        } else {
            (self.editor_scroll_origin_samples / max_scroll).clamp(0.0, 1.0) as f32
        }
    }

    fn beat_pixels(&self) -> f32 {
        (self.samples_per_beat() as f32 * self.pixels_per_sample()).max(0.01)
    }

    fn start_recording_preview(&mut self) {
        let sample = self.transport_samples.max(0.0) as usize;
        self.recording_preview_start_sample = Some(sample);
        self.recording_preview_sample = Some(sample);
        self.recording_preview_peaks.clear();
    }

    fn stop_recording_preview(&mut self) {
        self.recording_preview_start_sample = None;
        self.recording_preview_sample = None;
        self.recording_preview_peaks.clear();
    }

    fn recording_preview_bounds(&self) -> Option<(usize, usize)> {
        let start = self.recording_preview_start_sample?;
        let current = self.recording_preview_sample?;
        let (mut preview_start, mut preview_end) = if current > start {
            (start, current)
        } else {
            return None;
        };
        if self.punch_enabled
            && let Some((punch_start, punch_end)) = self.punch_range_samples
            && punch_end > punch_start
        {
            preview_start = preview_start.max(punch_start);
            preview_end = preview_end.min(punch_end);
        }
        if preview_end > preview_start {
            Some((preview_start, preview_end))
        } else {
            None
        }
    }

    fn audio_clip_key(
        track_name: &str,
        clip_name: &str,
        start: usize,
        length: usize,
        offset: usize,
    ) -> AudioClipKey {
        AudioClipKey {
            track_name: track_name.to_string(),
            clip_name: clip_name.to_string(),
            start,
            length,
            offset,
        }
    }

    fn sanitize_peak_file_component(value: &str) -> String {
        let mut out = String::with_capacity(value.len());
        for ch in value.chars() {
            if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
                out.push(ch);
            } else {
                out.push('_');
            }
        }
        if out.is_empty() {
            "clip".to_string()
        } else {
            out
        }
    }

    fn build_peak_file_rel(track_name: &str, clip_idx: usize, clip_name: &str) -> String {
        let track = Self::sanitize_peak_file_component(track_name);
        let clip = Self::sanitize_peak_file_component(clip_name);
        format!("peaks/{}_{:04}_{}.json", track, clip_idx, clip)
    }

    fn compute_audio_clip_peaks(path: &Path) -> std::io::Result<ClipPeaks> {
        let mut reader = WavReader::open(path).map_err(|e| {
            io::Error::other(format!("Failed to open WAV '{}': {e}", path.display()))
        })?;
        let spec = reader.spec();
        let channels = spec.channels.max(1) as usize;
        let samples: Vec<f32> = match spec.sample_format {
            SampleFormat::Float => reader.samples::<f32>().filter_map(|s| s.ok()).collect(),
            SampleFormat::Int => match spec.bits_per_sample {
                16 => reader
                    .samples::<i16>()
                    .filter_map(|s| s.ok())
                    .map(|s| s as f32 * (1.0 / 32768.0))
                    .collect(),
                24 => reader
                    .samples::<i32>()
                    .filter_map(|s| s.ok())
                    .map(|s| s as f32 * (1.0 / 8_388_608.0))
                    .collect(),
                32 => reader
                    .samples::<i32>()
                    .filter_map(|s| s.ok())
                    .map(|s| s as f32 * (1.0 / 2_147_483_648.0))
                    .collect(),
                _ => return Ok(Arc::new(Vec::new())),
            },
        };
        let mut per_channel = vec![Vec::with_capacity(samples.len() / channels + 1); channels];
        for frame in samples.chunks(channels) {
            for (channel_idx, sample) in frame.iter().enumerate() {
                per_channel[channel_idx].push(sample.clamp(-1.0, 1.0));
            }
        }

        if per_channel.iter().all(|ch| ch.is_empty()) {
            return Ok(Arc::new(Vec::new()));
        }

        let target_bins = per_channel
            .iter()
            .map(Vec::len)
            .max()
            .unwrap_or(0)
            .clamp(1024, MAX_PEAK_BINS);

        let mut peaks = vec![vec![[0.0_f32, 0.0_f32]; target_bins]; channels];
        for channel_idx in 0..channels {
            let samples = &per_channel[channel_idx];
            if samples.is_empty() {
                continue;
            }
            let mut touched = vec![false; target_bins];
            for (i, sample) in samples.iter().enumerate() {
                let bin = (i * target_bins) / samples.len();
                let idx = bin.min(target_bins - 1);
                if !touched[idx] {
                    peaks[channel_idx][idx] = [*sample, *sample];
                    touched[idx] = true;
                } else {
                    peaks[channel_idx][idx][0] = peaks[channel_idx][idx][0].min(*sample);
                    peaks[channel_idx][idx][1] = peaks[channel_idx][idx][1].max(*sample);
                }
            }
        }
        Ok(Arc::new(peaks))
    }

    fn read_clip_peaks_file(path: &Path) -> std::io::Result<ClipPeaks> {
        let file = File::open(path)?;
        let json: Value = serde_json::from_reader(BufReader::new(file))?;
        let peaks_val = json.get("peaks").cloned().unwrap_or(Value::Null);
        let Some(root) = peaks_val.as_array() else {
            return Ok(Arc::new(Vec::new()));
        };

        if root.first().is_some_and(Value::is_number) {
            let mono = root
                .iter()
                .filter_map(Value::as_f64)
                .map(|v| {
                    let a = (v as f32).abs().clamp(0.0, 1.0);
                    [-a, a]
                })
                .collect::<Vec<_>>();
            return Ok(Arc::new(if mono.is_empty() {
                Vec::new()
            } else {
                vec![mono]
            }));
        }

        let first = root.first();
        if first.is_some_and(|v| {
            v.as_array()
                .is_some_and(|a| a.len() == 2 && a[0].is_number() && a[1].is_number())
        }) {
            let mono = root
                .iter()
                .filter_map(Value::as_array)
                .filter_map(|pair| {
                    let min = pair.first()?.as_f64()? as f32;
                    let max = pair.get(1)?.as_f64()? as f32;
                    Some([min.min(max).clamp(-1.0, 1.0), min.max(max).clamp(-1.0, 1.0)])
                })
                .collect::<Vec<_>>();
            return Ok(Arc::new(if mono.is_empty() {
                Vec::new()
            } else {
                vec![mono]
            }));
        }

        let mut per_channel: Vec<Vec<[f32; 2]>> = Vec::with_capacity(root.len());
        for channel in root {
            let Some(arr) = channel.as_array() else {
                continue;
            };
            let mut ch = Vec::with_capacity(arr.len());
            for peak in arr {
                if let Some(pair) = peak.as_array()
                    && pair.len() == 2
                    && let (Some(min), Some(max)) = (
                        pair.first().and_then(Value::as_f64),
                        pair.get(1).and_then(Value::as_f64),
                    )
                {
                    let min = min as f32;
                    let max = max as f32;
                    ch.push([min.min(max).clamp(-1.0, 1.0), min.max(max).clamp(-1.0, 1.0)]);
                    continue;
                }
                if let Some(v) = peak.as_f64() {
                    let a = (v as f32).abs().clamp(0.0, 1.0);
                    ch.push([-a, a]);
                }
            }
            per_channel.push(ch);
        }
        Ok(Arc::new(per_channel))
    }

    fn stream_peak_file_to_queue(
        peaks_path: &Path,
        track_name: String,
        clip_name: String,
        start: usize,
        length: usize,
        offset: usize,
    ) -> std::io::Result<()> {
        let peaks = Self::read_clip_peaks_file(peaks_path)?;
        if peaks.is_empty() {
            if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
                queue.push(AudioPeakChunkUpdate {
                    track_name,
                    clip_name,
                    start,
                    length,
                    offset,
                    channels: 0,
                    target_bins: 0,
                    bin_start: 0,
                    peaks: Vec::new(),
                    done: true,
                });
            }
            return Ok(());
        }

        let channels = peaks.len();
        let target_bins = peaks.first().map(Vec::len).unwrap_or(0);
        if target_bins == 0 {
            if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
                queue.push(AudioPeakChunkUpdate {
                    track_name,
                    clip_name,
                    start,
                    length,
                    offset,
                    channels,
                    target_bins: 0,
                    bin_start: 0,
                    peaks: Vec::new(),
                    done: true,
                });
            }
            return Ok(());
        }

        let mut bin_start = 0usize;
        while bin_start < target_bins {
            let end = (bin_start + BINS_PER_UPDATE).min(target_bins);
            let mut chunk = vec![Vec::with_capacity(end - bin_start); channels];
            for (channel_idx, chunk_channel) in chunk.iter_mut().enumerate().take(channels) {
                if let Some(channel) = peaks.get(channel_idx) {
                    chunk_channel.extend_from_slice(&channel[bin_start..end]);
                }
            }
            if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
                queue.push(AudioPeakChunkUpdate {
                    track_name: track_name.clone(),
                    clip_name: clip_name.clone(),
                    start,
                    length,
                    offset,
                    channels,
                    target_bins,
                    bin_start,
                    peaks: chunk,
                    done: false,
                });
            }
            bin_start = end;
        }

        if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
            queue.push(AudioPeakChunkUpdate {
                track_name,
                clip_name,
                start,
                length,
                offset,
                channels,
                target_bins,
                bin_start: 0,
                peaks: Vec::new(),
                done: true,
            });
        }
        Ok(())
    }

    fn stream_audio_clip_peaks_to_queue(
        path: &Path,
        track_name: String,
        clip_name: String,
        start: usize,
        length: usize,
        offset: usize,
    ) -> std::io::Result<()> {
        let mut reader = WavReader::open(path).map_err(|e| {
            io::Error::other(format!("Failed to open WAV '{}': {e}", path.display()))
        })?;
        let spec = reader.spec();
        let channels = spec.channels.max(1) as usize;
        let total_samples = reader.duration() as usize;
        let total_frames = total_samples / channels.max(1);
        if total_frames == 0 {
            if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
                queue.push(AudioPeakChunkUpdate {
                    track_name,
                    clip_name,
                    start,
                    length,
                    offset,
                    channels,
                    target_bins: 0,
                    bin_start: 0,
                    peaks: Vec::new(),
                    done: true,
                });
            }
            return Ok(());
        }

        let target_bins = total_frames.clamp(1024, MAX_PEAK_BINS);
        let mut accum = vec![vec![[0.0_f32, 0.0_f32]; target_bins]; channels];
        let mut touched = vec![vec![false; target_bins]; channels];
        let mut processed_frames = 0usize;
        let mut last_emitted_bin = 0usize;

        let mut sample_iter: Box<dyn Iterator<Item = f32>> = match spec.sample_format {
            SampleFormat::Float => Box::new(reader.samples::<f32>().filter_map(|s| s.ok())),
            SampleFormat::Int => match spec.bits_per_sample {
                16 => Box::new(
                    reader
                        .samples::<i16>()
                        .filter_map(|s| s.ok())
                        .map(|s| s as f32 * (1.0 / 32768.0)),
                ),
                24 => Box::new(
                    reader
                        .samples::<i32>()
                        .filter_map(|s| s.ok())
                        .map(|s| s as f32 * (1.0 / 8_388_608.0)),
                ),
                32 => Box::new(
                    reader
                        .samples::<i32>()
                        .filter_map(|s| s.ok())
                        .map(|s| s as f32 * (1.0 / 2_147_483_648.0)),
                ),
                _ => return Ok(()),
            },
        };

        while processed_frames < total_frames {
            let frames_to_read = (total_frames - processed_frames).min(CHUNK_FRAMES);
            let samples_to_read = frames_to_read.saturating_mul(channels);
            let chunk: Vec<f32> = sample_iter.by_ref().take(samples_to_read).collect();
            if chunk.is_empty() {
                break;
            }
            let frames_read = chunk.len() / channels.max(1);
            if frames_read == 0 {
                break;
            }

            for (frame_offset, frame) in chunk.chunks(channels).enumerate() {
                let frame_index = processed_frames + frame_offset;
                let bin = ((frame_index * target_bins) / total_frames).min(target_bins - 1);
                for (channel_idx, sample) in frame.iter().enumerate() {
                    let s = sample.clamp(-1.0, 1.0);
                    if !touched[channel_idx][bin] {
                        accum[channel_idx][bin] = [s, s];
                        touched[channel_idx][bin] = true;
                    } else {
                        accum[channel_idx][bin][0] = accum[channel_idx][bin][0].min(s);
                        accum[channel_idx][bin][1] = accum[channel_idx][bin][1].max(s);
                    }
                }
            }

            processed_frames = processed_frames.saturating_add(frames_read);
            let emit_end = (((processed_frames * target_bins) / total_frames) + 1).min(target_bins);
            if emit_end > last_emitted_bin {
                let mut peaks_chunk =
                    vec![Vec::with_capacity(emit_end - last_emitted_bin); channels];
                for channel_idx in 0..channels {
                    for bin in last_emitted_bin..emit_end {
                        let pair = if touched[channel_idx][bin] {
                            accum[channel_idx][bin]
                        } else {
                            [0.0_f32, 0.0_f32]
                        };
                        peaks_chunk[channel_idx].push(pair);
                    }
                }
                if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
                    queue.push(AudioPeakChunkUpdate {
                        track_name: track_name.clone(),
                        clip_name: clip_name.clone(),
                        start,
                        length,
                        offset,
                        channels,
                        target_bins,
                        bin_start: last_emitted_bin,
                        peaks: peaks_chunk,
                        done: false,
                    });
                }
                last_emitted_bin = emit_end;
            }
        }

        if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
            queue.push(AudioPeakChunkUpdate {
                track_name,
                clip_name,
                start,
                length,
                offset,
                channels,
                target_bins,
                bin_start: 0,
                peaks: Vec::new(),
                done: true,
            });
        }
        Ok(())
    }

    fn audio_clip_source_length(path: &Path) -> std::io::Result<usize> {
        let reader = WavReader::open(path).map_err(|e| {
            io::Error::other(format!("Failed to open WAV '{}': {e}", path.display()))
        })?;
        let spec = reader.spec();
        let total_samples = reader.duration() as usize;
        Ok(total_samples / spec.channels.max(1) as usize)
    }

    fn file_extension_lower(path: &Path) -> Option<String> {
        path.extension()
            .and_then(|ext| ext.to_str())
            .map(|ext| ext.trim_matches('.').to_ascii_lowercase())
    }

    fn normalize_session_media_rel(path: &str) -> Option<String> {
        let candidate = Path::new(path);
        if candidate.is_absolute() {
            return None;
        }

        let mut parts = Vec::new();
        for component in candidate.components() {
            match component {
                std::path::Component::Normal(part) => {
                    parts.push(part.to_string_lossy().to_string());
                }
                std::path::Component::CurDir => {}
                _ => return None,
            }
        }

        if parts.is_empty() {
            None
        } else {
            Some(parts.join("/"))
        }
    }

    fn is_cleanup_target_rel(path: &str) -> bool {
        if path.starts_with("audio/") || path.starts_with("midi/") {
            let rel_path = Path::new(path);
            let Some(ext) = Self::file_extension_lower(rel_path) else {
                return false;
            };
            if matches!(ext.as_str(), "wav" | "mid" | "midi") {
                return true;
            }
            if path.starts_with("audio/") && ext == "txt" {
                let stem = rel_path
                    .file_stem()
                    .and_then(|stem| stem.to_str())
                    .unwrap_or("");
                return stem.contains("_pitchmap");
            }
            return false;
        }

        (path.starts_with("peaks/") || path.starts_with("pitch/"))
            && Path::new(path).extension().and_then(|ext| ext.to_str()) == Some("json")
    }

    fn insert_referenced_session_media_path(referenced: &mut HashSet<String>, path: &str) {
        let Some(rel) = Self::normalize_session_media_rel(path) else {
            return;
        };
        if Self::is_cleanup_target_rel(&rel) {
            referenced.insert(rel);
        }
    }

    fn pitch_correction_cache_rel(
        source_name: &str,
        source_offset: usize,
        source_length: usize,
    ) -> String {
        let mut hasher = DefaultHasher::new();
        source_name.hash(&mut hasher);
        source_offset.hash(&mut hasher);
        source_length.hash(&mut hasher);
        format!("pitch/{:016x}.json", hasher.finish())
    }

    fn insert_pitch_correction_cache_reference(
        referenced: &mut HashSet<String>,
        clip: &crate::state::AudioClip,
    ) {
        let Some(source_name) = clip.pitch_correction_source_name.as_deref() else {
            return;
        };
        let Some(source_rel) = Self::normalize_session_media_rel(source_name) else {
            return;
        };
        let cache_rel = Self::pitch_correction_cache_rel(
            &source_rel,
            clip.pitch_correction_source_offset.unwrap_or(clip.offset),
            clip.pitch_correction_source_length.unwrap_or(clip.length),
        );
        referenced.insert(cache_rel);
    }

    fn referenced_session_media_paths(state: &crate::state::StateData) -> HashSet<String> {
        let mut referenced = HashSet::new();

        for track in &state.tracks {
            for clip in &track.audio.clips {
                Self::insert_referenced_session_media_path(&mut referenced, &clip.name);
                if let Some(source_name) = clip.pitch_correction_source_name.as_deref() {
                    Self::insert_referenced_session_media_path(&mut referenced, source_name);
                }
                if let Some(peaks_file) = clip.peaks_file.as_deref() {
                    Self::insert_referenced_session_media_path(&mut referenced, peaks_file);
                }
                Self::insert_pitch_correction_cache_reference(&mut referenced, clip);
            }
            for clip in &track.midi.clips {
                Self::insert_referenced_session_media_path(&mut referenced, &clip.name);
            }
            for clip in &track.frozen_audio_backup {
                Self::insert_referenced_session_media_path(&mut referenced, &clip.name);
                if let Some(source_name) = clip.pitch_correction_source_name.as_deref() {
                    Self::insert_referenced_session_media_path(&mut referenced, source_name);
                }
                if let Some(peaks_file) = clip.peaks_file.as_deref() {
                    Self::insert_referenced_session_media_path(&mut referenced, peaks_file);
                }
                Self::insert_pitch_correction_cache_reference(&mut referenced, clip);
            }
            for clip in &track.frozen_midi_backup {
                Self::insert_referenced_session_media_path(&mut referenced, &clip.name);
            }
            if let Some(render_clip) = track.frozen_render_clip.as_deref() {
                Self::insert_referenced_session_media_path(&mut referenced, render_clip);
            }
        }

        referenced
    }

    fn collect_cleanup_candidate_files(
        dir: &Path,
        session_root: &Path,
        referenced: &HashSet<String>,
        out: &mut Vec<(PathBuf, String)>,
    ) -> io::Result<()> {
        let entries = match fs::read_dir(dir) {
            Ok(entries) => entries,
            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
            Err(err) => return Err(err),
        };

        for entry in entries {
            let entry = entry?;
            let path = entry.path();
            let file_type = entry.file_type()?;
            if file_type.is_dir() {
                Self::collect_cleanup_candidate_files(&path, session_root, referenced, out)?;
                continue;
            }
            if !file_type.is_file() {
                continue;
            }

            let Ok(rel_path) = path.strip_prefix(session_root) else {
                continue;
            };
            let rel = rel_path.to_string_lossy().replace('\\', "/");
            if Self::is_cleanup_target_rel(&rel) && !referenced.contains(&rel) {
                out.push((path, rel));
            }
        }

        Ok(())
    }

    fn delete_unused_session_media_files(
        &self,
        session_root: &Path,
    ) -> Result<SessionMediaCleanupReport, String> {
        let referenced = {
            let state = self.state.blocking_read();
            Self::referenced_session_media_paths(&state)
        };

        let mut candidates = Vec::new();
        Self::collect_cleanup_candidate_files(
            &session_root.join("audio"),
            session_root,
            &referenced,
            &mut candidates,
        )
        .map_err(|e| format!("Failed to scan audio/: {e}"))?;
        Self::collect_cleanup_candidate_files(
            &session_root.join("midi"),
            session_root,
            &referenced,
            &mut candidates,
        )
        .map_err(|e| format!("Failed to scan midi/: {e}"))?;
        Self::collect_cleanup_candidate_files(
            &session_root.join("peaks"),
            session_root,
            &referenced,
            &mut candidates,
        )
        .map_err(|e| format!("Failed to scan peaks/: {e}"))?;
        Self::collect_cleanup_candidate_files(
            &session_root.join("pitch"),
            session_root,
            &referenced,
            &mut candidates,
        )
        .map_err(|e| format!("Failed to scan pitch/: {e}"))?;
        candidates.sort_by(|a, b| a.1.cmp(&b.1));

        let mut report = SessionMediaCleanupReport::default();
        for (path, rel) in candidates {
            match fs::remove_file(&path) {
                Ok(()) => report.deleted_files.push(rel),
                Err(e) => report.failed_files.push(format!("{rel} ({e})")),
            }
        }

        Ok(report)
    }

    fn is_import_audio_path(path: &Path) -> bool {
        matches!(
            Self::file_extension_lower(path).as_deref(),
            Some("wav" | "ogg" | "mp3" | "flac")
        )
    }

    fn is_import_midi_path(path: &Path) -> bool {
        matches!(
            Self::file_extension_lower(path).as_deref(),
            Some("mid" | "midi")
        )
    }

    fn import_track_base_name(path: &Path) -> String {
        let stem = path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("Track")
            .trim();
        let candidate = Self::sanitize_peak_file_component(stem);
        if candidate.is_empty() {
            "Track".to_string()
        } else {
            candidate
        }
    }

    fn sanitize_generated_track_base_name(prompt: &str) -> String {
        let prompt = prompt.trim();
        if prompt.is_empty() {
            return "Generated".to_string();
        }
        let shortened = prompt.chars().take(32).collect::<String>();
        let candidate = Self::sanitize_peak_file_component(&shortened);
        if candidate.is_empty() {
            "Generated".to_string()
        } else {
            candidate
        }
    }

    fn generate_audio_tags_with_timing(
        tags: &str,
        bpm: f32,
        time_signature_num: u8,
        time_signature_denom: u8,
    ) -> String {
        let mut parts = tags
            .split(',')
            .map(str::trim)
            .filter(|tag| !tag.is_empty())
            .map(str::to_owned)
            .collect::<Vec<_>>();
        parts.push(format!("{:.0}bpm", bpm.clamp(20.0, 300.0)));
        parts.push(format!(
            "{}/{} tempo signature",
            time_signature_num.max(1),
            time_signature_denom.max(1)
        ));
        parts.join(",")
    }

    fn unique_track_name(base: &str, used_names: &mut HashSet<String>) -> String {
        if !used_names.contains(base) {
            let candidate = base.to_string();
            used_names.insert(candidate.clone());
            return candidate;
        }
        for n in 2..=9_999 {
            let candidate = format!("{base}_{n}");
            if !used_names.contains(&candidate) {
                used_names.insert(candidate.clone());
                return candidate;
            }
        }
        let fallback = format!("{base}_import");
        used_names.insert(fallback.clone());
        fallback
    }

    fn unique_import_rel_path(
        session_root: &Path,
        subdir: &str,
        stem: &str,
        ext: &str,
    ) -> std::io::Result<String> {
        fs::create_dir_all(session_root.join(subdir))?;
        let clean_stem = Self::sanitize_peak_file_component(stem);
        let clean_ext = ext.trim_matches('.').to_ascii_lowercase();
        let mut index = 0usize;
        loop {
            let file_name = if index == 0 {
                format!("{clean_stem}.{clean_ext}")
            } else {
                format!("{clean_stem}_{index:03}.{clean_ext}")
            };
            let rel = format!("{subdir}/{file_name}");
            if !session_root.join(&rel).exists() {
                return Ok(rel);
            }
            index = index.saturating_add(1);
        }
    }

    async fn decode_audio_to_f32_interleaved_with_progress<F>(
        path: &Path,
        mut progress_callback: F,
    ) -> std::io::Result<(Vec<f32>, usize, u32)>
    where
        F: FnMut(f32),
    {
        let file = File::open(path)?;
        let file_size = file.metadata()?.len();
        let mss = MediaSourceStream::new(Box::new(file), Default::default());
        let mut hint = Hint::new();
        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
            hint.with_extension(ext);
        }
        let probed = symphonia::default::get_probe()
            .format(
                &hint,
                mss,
                &FormatOptions::default(),
                &MetadataOptions::default(),
            )
            .map_err(|e| {
                io::Error::other(format!(
                    "Unsupported or unreadable audio '{}': {e}",
                    path.display()
                ))
            })?;
        let mut format = probed.format;
        let track = format.default_track().ok_or_else(|| {
            io::Error::other(format!("No decodable audio track in '{}'", path.display()))
        })?;
        let mut decoder = symphonia::default::get_codecs()
            .make(&track.codec_params, &DecoderOptions::default())
            .map_err(|e| io::Error::other(format!("Failed to decode '{}': {e}", path.display())))?;
        let mut channels = track
            .codec_params
            .channels
            .map(|c| c.count())
            .unwrap_or(1usize)
            .max(1);
        let mut sample_rate = track.codec_params.sample_rate.unwrap_or(48_000u32);
        let mut samples = Vec::<f32>::new();

        let mut packets_decoded = 0usize;
        let report_interval = 100;

        loop {
            let packet = match format.next_packet() {
                Ok(packet) => packet,
                Err(SymphoniaError::IoError(e))
                    if e.kind() == std::io::ErrorKind::UnexpectedEof =>
                {
                    break;
                }
                Err(SymphoniaError::ResetRequired) => {
                    return Err(io::Error::other(format!(
                        "Decoder reset required for '{}'",
                        path.display()
                    )));
                }
                Err(e) => {
                    return Err(io::Error::other(format!(
                        "Failed reading audio packets '{}': {e}",
                        path.display()
                    )));
                }
            };
            let decoded = match decoder.decode(&packet) {
                Ok(decoded) => decoded,
                Err(SymphoniaError::DecodeError(_)) => continue,
                Err(SymphoniaError::ResetRequired) => {
                    return Err(io::Error::other(format!(
                        "Decoder reset required for '{}'",
                        path.display()
                    )));
                }
                Err(e) => {
                    return Err(io::Error::other(format!(
                        "Audio decode failed '{}': {e}",
                        path.display()
                    )));
                }
            };
            let spec = *decoded.spec();
            channels = spec.channels.count().max(1);
            sample_rate = spec.rate;
            let mut sample_buffer = SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
            sample_buffer.copy_interleaved_ref(decoded);
            samples.extend_from_slice(sample_buffer.samples());

            packets_decoded += 1;
            if packets_decoded.is_multiple_of(report_interval) {
                let bytes_read = samples.len() * std::mem::size_of::<f32>();
                let progress = if file_size > 0 {
                    (bytes_read as f64 / file_size as f64).clamp(0.0, 1.0) as f32
                } else {
                    0.0
                };
                progress_callback(progress);
                tokio::task::yield_now().await;
            }
        }

        if samples.is_empty() {
            return Err(io::Error::other(format!(
                "Audio file '{}' contains no samples",
                path.display()
            )));
        }
        progress_callback(1.0);
        Ok((samples, channels, sample_rate))
    }

    async fn resample_audio_interleaved_with_progress<F>(
        samples: &[f32],
        channels: usize,
        from_rate: u32,
        to_rate: u32,
        mut progress_callback: F,
    ) -> std::io::Result<Vec<f32>>
    where
        F: FnMut(f32),
    {
        if from_rate == to_rate {
            progress_callback(1.0);
            return Ok(samples.to_vec());
        }

        use rubato::{
            Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType,
            WindowFunction,
        };

        let params = SincInterpolationParameters {
            sinc_len: 256,
            f_cutoff: 0.95,
            interpolation: SincInterpolationType::Linear,
            oversampling_factor: 256,
            window: WindowFunction::BlackmanHarris2,
        };

        progress_callback(0.1);

        let mut resampler = SincFixedIn::<f32>::new(
            to_rate as f64 / from_rate as f64,
            2.0,
            params,
            samples.len() / channels.max(1),
            channels.max(1),
        )
        .map_err(|e| io::Error::other(format!("Failed to create resampler: {e}")))?;

        progress_callback(0.2);
        tokio::task::yield_now().await;

        let frames = samples.len() / channels.max(1);
        let mut channel_buffers: Vec<Vec<f32>> = vec![Vec::with_capacity(frames); channels];

        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
        unsafe {
            if channels == 2 && std::arch::is_x86_feature_detected!("sse") {
                channel_buffers[0].resize(frames, 0.0);
                channel_buffers[1].resize(frames, 0.0);
                let n = frames / 4;
                for i in 0..n {
                    let src = std::arch::x86_64::_mm_loadu_ps(samples.as_ptr().add(i * 8));
                    let src2 = std::arch::x86_64::_mm_loadu_ps(samples.as_ptr().add(i * 8 + 4));
                    let left = std::arch::x86_64::_mm_shuffle_ps(src, src2, 0b10_00_10_00);
                    let right = std::arch::x86_64::_mm_shuffle_ps(src, src2, 0b11_01_11_01);
                    std::arch::x86_64::_mm_storeu_ps(
                        channel_buffers[0].as_mut_ptr().add(i * 4),
                        left,
                    );
                    std::arch::x86_64::_mm_storeu_ps(
                        channel_buffers[1].as_mut_ptr().add(i * 4),
                        right,
                    );
                }
                for i in n * 4..frames {
                    channel_buffers[0][i] = samples[i * 2];
                    channel_buffers[1][i] = samples[i * 2 + 1];
                }
            } else {
                for (i, &sample) in samples.iter().enumerate() {
                    channel_buffers[i % channels].push(sample);
                }
            }
        }
        #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
        {
            for (i, &sample) in samples.iter().enumerate() {
                channel_buffers[i % channels].push(sample);
            }
        }

        progress_callback(0.5);
        tokio::task::yield_now().await;

        let resampled = resampler
            .process(&channel_buffers, None)
            .map_err(|e| io::Error::other(format!("Resampling failed: {e}")))?;

        progress_callback(0.8);
        tokio::task::yield_now().await;

        let out_frames = resampled[0].len();
        let mut output: Vec<f32> = Vec::with_capacity(out_frames * channels);

        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
        unsafe {
            if channels == 2 && std::arch::is_x86_feature_detected!("sse") {
                output.set_len(out_frames * 2);
                let left = &resampled[0];
                let right = &resampled[1];
                let n = out_frames / 4;
                for i in 0..n {
                    let l = std::arch::x86_64::_mm_loadu_ps(left.as_ptr().add(i * 4));
                    let r = std::arch::x86_64::_mm_loadu_ps(right.as_ptr().add(i * 4));
                    let lr0 = std::arch::x86_64::_mm_unpacklo_ps(l, r);
                    let lr1 = std::arch::x86_64::_mm_unpackhi_ps(l, r);
                    std::arch::x86_64::_mm_storeu_ps(output.as_mut_ptr().add(i * 8), lr0);
                    std::arch::x86_64::_mm_storeu_ps(output.as_mut_ptr().add(i * 8 + 4), lr1);
                }
                for i in n * 4..out_frames {
                    output[i * 2] = left[i];
                    output[i * 2 + 1] = right[i];
                }
            } else {
                for frame_idx in 0..out_frames {
                    for channel in resampled.iter().take(channels) {
                        output.push(channel[frame_idx]);
                    }
                }
            }
        }
        #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
        {
            for frame_idx in 0..out_frames {
                for channel in resampled.iter().take(channels) {
                    output.push(channel[frame_idx]);
                }
            }
        }

        progress_callback(1.0);
        Ok(output)
    }

    async fn import_audio_to_session_wav_with_progress<F>(
        src_path: &Path,
        session_root: &Path,
        target_sample_rate: u32,
        mut progress_callback: F,
    ) -> std::io::Result<(String, usize, usize, ClipPeaks)>
    where
        F: FnMut(f32, Option<String>),
    {
        let stem = src_path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("audio");
        let rel = Self::unique_import_rel_path(session_root, "audio", stem, "wav")?;
        let dst = session_root.join(&rel);

        let (samples, channels, sample_rate) =
            Self::decode_audio_to_f32_interleaved_with_progress(src_path, |decode_progress| {
                progress_callback(decode_progress * 0.6, Some("Decoding".to_string()));
            })
            .await?;

        progress_callback(0.6, None);
        tokio::task::yield_now().await;

        let final_samples = if sample_rate != target_sample_rate {
            let resample_msg = format!("Resampling {} Hz → {} Hz", sample_rate, target_sample_rate);
            Self::resample_audio_interleaved_with_progress(
                &samples,
                channels,
                sample_rate,
                target_sample_rate,
                |resample_progress| {
                    progress_callback(0.6 + resample_progress * 0.25, Some(resample_msg.clone()));
                },
            )
            .await?
        } else {
            samples
        };

        progress_callback(0.85, Some("Writing".to_string()));
        tokio::task::yield_now().await;

        {
            let spec = WavSpec {
                channels: channels as u16,
                sample_rate: target_sample_rate,
                bits_per_sample: 32,
                sample_format: SampleFormat::Float,
            };
            let mut writer = hound::WavWriter::create(&dst, spec).map_err(|e| {
                io::Error::other(format!(
                    "Failed to create WAV writer for '{}': {e}",
                    dst.display()
                ))
            })?;
            for sample in &final_samples {
                writer.write_sample(*sample).map_err(|e| {
                    io::Error::other(format!(
                        "Failed to write sample to '{}': {e}",
                        dst.display()
                    ))
                })?;
            }
            writer.finalize().map_err(|e| {
                io::Error::other(format!("Failed to finalize WAV '{}': {e}", dst.display()))
            })?;
        }

        progress_callback(0.95, Some("Calculating peaks".to_string()));
        tokio::task::yield_now().await;
        let peaks = Self::compute_audio_clip_peaks(&dst)?;

        progress_callback(1.0, None);
        let frames = final_samples.len() / channels.max(1);
        Ok((rel, channels.max(1), frames.max(1), peaks))
    }

    async fn stretch_audio_clip_with_rubberband(
        src_path: &Path,
        session_root: &Path,
        clip_name: &str,
        offset: usize,
        length: usize,
        stretch_ratio: f32,
    ) -> io::Result<(String, usize)> {
        let (samples, channels, sample_rate) =
            Self::decode_audio_to_f32_interleaved_with_progress(src_path, |_| {}).await?;
        let channels = channels.max(1);
        let total_frames = samples.len() / channels;
        let start_frame = offset.min(total_frames);
        let segment_frames = length.max(1).min(total_frames.saturating_sub(start_frame));
        if segment_frames == 0 {
            return Err(io::Error::other(format!(
                "Audio clip '{}' has no available source samples to stretch",
                clip_name
            )));
        }

        let start_idx = start_frame * channels;
        let end_idx = start_idx + segment_frames * channels;
        let segment_samples = &samples[start_idx..end_idx];
        let stem = Path::new(clip_name)
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("audio");
        let temp_rel =
            Self::unique_import_rel_path(session_root, "audio", &format!("{stem}_src"), "wav")?;
        let temp_path = session_root.join(&temp_rel);
        let output_rel =
            Self::unique_import_rel_path(session_root, "audio", &format!("{stem}_stretch"), "wav")?;
        let output_path = session_root.join(&output_rel);

        {
            let spec = WavSpec {
                channels: channels as u16,
                sample_rate,
                bits_per_sample: 32,
                sample_format: SampleFormat::Float,
            };
            let mut writer = hound::WavWriter::create(&temp_path, spec).map_err(|e| {
                io::Error::other(format!(
                    "Failed to create WAV writer for '{}': {e}",
                    temp_path.display()
                ))
            })?;
            for sample in segment_samples {
                writer.write_sample(*sample).map_err(|e| {
                    io::Error::other(format!(
                        "Failed to write sample to '{}': {e}",
                        temp_path.display()
                    ))
                })?;
            }
            writer.finalize().map_err(|e| {
                io::Error::other(format!(
                    "Failed to finalize WAV '{}': {e}",
                    temp_path.display()
                ))
            })?;
        }

        let command_result = tokio::process::Command::new("rubberband")
            .arg("--quiet")
            .arg("--fine")
            .arg("--time")
            .arg(format!("{:.6}", stretch_ratio.max(0.01)))
            .arg(&temp_path)
            .arg(&output_path)
            .output()
            .await;

        let _ = fs::remove_file(&temp_path);

        let output = command_result
            .map_err(|e| io::Error::other(format!("Failed to launch Rubber Band: {e}")))?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
            let _ = fs::remove_file(&output_path);
            return Err(io::Error::other(if stderr.is_empty() {
                "Rubber Band failed to stretch the clip".to_string()
            } else {
                format!("Rubber Band failed: {stderr}")
            }));
        }

        let output_frames = Self::audio_clip_source_length(&output_path)?;
        Ok((output_rel, output_frames.max(1)))
    }

    async fn analyze_audio_clip_pitch_correction<F>(
        src_path: &Path,
        clip_name: &str,
        offset: usize,
        length: usize,
        frame_likeness: f32,
        mut progress_callback: F,
    ) -> io::Result<PitchCorrectionData>
    where
        F: FnMut(f32, Option<String>),
    {
        let (samples, channels, sample_rate) =
            Self::decode_audio_to_f32_interleaved_with_progress(src_path, |decode_progress| {
                progress_callback(decode_progress * 0.55, Some("Decoding".to_string()));
            })
            .await?;
        let channels = channels.max(1);
        let total_frames = samples.len() / channels;
        let start_frame = offset.min(total_frames);
        let segment_frames = length.max(1).min(total_frames.saturating_sub(start_frame));
        if segment_frames < 256 {
            return Err(io::Error::other(format!(
                "Audio clip '{}' is too short for pitch analysis",
                clip_name
            )));
        }

        let mut mono = Vec::with_capacity(segment_frames);
        for frame_idx in 0..segment_frames {
            let base = (start_frame + frame_idx) * channels;
            let sample = (0..channels)
                .map(|channel| samples[base + channel])
                .sum::<f32>()
                / channels as f32;
            mono.push(sample);
        }
        progress_callback(0.60, Some("Preparing analysis".to_string()));

        let analysis_size = 2048usize.min(mono.len().next_power_of_two() / 2).max(256);
        let analysis_size = analysis_size.min(mono.len());
        let analysis_size = analysis_size
            .next_power_of_two()
            .min(mono.len().next_power_of_two());
        let analysis_size = analysis_size.min(mono.len());
        let analysis_size = analysis_size.clamp(256, 4096);
        if mono.len() < analysis_size {
            return Err(io::Error::other(format!(
                "Audio clip '{}' is too short for pitch analysis",
                clip_name
            )));
        }
        let hop_size = (analysis_size / 4).max(64);
        let padding = analysis_size / 2;
        let mut detector = McLeodDetector::new(analysis_size, padding);
        let mut detected = Vec::<PitchCorrectionPoint>::new();
        let mut cursor = 0usize;
        let total_windows = ((mono.len().saturating_sub(analysis_size)) / hop_size).max(1) + 1;
        let mut window_index = 0usize;
        while cursor + analysis_size <= mono.len() {
            let window: Vec<f64> = mono[cursor..cursor + analysis_size]
                .iter()
                .map(|sample| *sample as f64)
                .collect();
            if let Some(pitch) = detector.get_pitch(&window, sample_rate as usize, 5.0f64, 0.7f64)
                && pitch.frequency.is_finite()
                && pitch.frequency > 0.0
            {
                let midi_pitch = 69.0 + 12.0 * (pitch.frequency as f32 / 440.0).log2();
                if midi_pitch.is_finite() {
                    detected.push(PitchCorrectionPoint {
                        start_sample: cursor,
                        length_samples: hop_size.min(mono.len().saturating_sub(cursor)).max(1),
                        detected_midi_pitch: midi_pitch.clamp(0.0, f32::from(PITCH_MAX) + 0.999),
                        target_midi_pitch: midi_pitch.clamp(0.0, f32::from(PITCH_MAX) + 0.999),
                        clarity: pitch.clarity as f32,
                    });
                }
            }
            window_index = window_index.saturating_add(1);
            let analysis_progress = window_index as f32 / total_windows as f32;
            progress_callback(
                0.60 + analysis_progress.clamp(0.0, 1.0) * 0.40,
                Some("Detecting pitch".to_string()),
            );
            cursor = cursor.saturating_add(hop_size);
        }
        if detected.is_empty() {
            return Err(io::Error::other(format!(
                "No stable pitch detected in '{}'",
                clip_name
            )));
        }
        let frame_likeness = frame_likeness.clamp(0.05, 2.0);
        let raw_points = detected;
        let points =
            Self::merge_adjacent_pitch_fragments(raw_points.clone(), frame_likeness, hop_size);
        Ok(PitchCorrectionData {
            track_idx: String::new(),
            clip_index: 0,
            clip_name: clip_name.to_string(),
            clip_length_samples: length.max(1),
            frame_likeness,
            raw_points,
            points,
        })
    }

    fn regroup_pitch_correction_frames(
        pitch_correction: &mut PitchCorrectionData,
        frame_likeness: f32,
    ) {
        let clamped = frame_likeness.clamp(0.05, 2.0);
        let max_gap_samples = pitch_correction
            .raw_points
            .iter()
            .map(|point| point.length_samples)
            .min()
            .unwrap_or(1)
            .max(1);
        pitch_correction.frame_likeness = clamped;
        pitch_correction.points = Self::merge_adjacent_pitch_fragments(
            pitch_correction.raw_points.clone(),
            clamped,
            max_gap_samples,
        );
    }

    fn merge_adjacent_pitch_fragments(
        mut points: Vec<PitchCorrectionPoint>,
        max_pitch_delta: f32,
        max_gap_samples: usize,
    ) -> Vec<PitchCorrectionPoint> {
        if points.len() <= 1 {
            return points;
        }
        points.sort_by_key(|point| point.start_sample);
        let mut merged = Vec::with_capacity(points.len());
        let mut points_iter = points.into_iter();
        let Some(mut current) = points_iter.next() else {
            return merged;
        };
        let mut reference_detected = current.detected_midi_pitch;
        let mut reference_target = current.target_midi_pitch;

        for point in points_iter {
            let current_end = current.start_sample.saturating_add(current.length_samples);
            let gap = point.start_sample.saturating_sub(current_end);
            let detected_delta = (reference_detected - point.detected_midi_pitch).abs();
            let target_delta = (reference_target - point.target_midi_pitch).abs();

            if gap <= max_gap_samples
                && detected_delta <= max_pitch_delta
                && target_delta <= max_pitch_delta
            {
                let left_weight = (current.length_samples as f32) * current.clarity.max(0.05);
                let right_weight = (point.length_samples as f32) * point.clarity.max(0.05);
                let total_weight = (left_weight + right_weight).max(f32::EPSILON);
                let merged_end = point.start_sample.saturating_add(point.length_samples);
                current.length_samples = merged_end.saturating_sub(current.start_sample);
                current.detected_midi_pitch = ((current.detected_midi_pitch * left_weight)
                    + (point.detected_midi_pitch * right_weight))
                    / total_weight;
                current.target_midi_pitch = ((current.target_midi_pitch * left_weight)
                    + (point.target_midi_pitch * right_weight))
                    / total_weight;
                current.clarity = ((current.clarity * left_weight)
                    + (point.clarity * right_weight))
                    / total_weight;
            } else {
                merged.push(current);
                reference_detected = point.detected_midi_pitch;
                reference_target = point.target_midi_pitch;
                current = point;
            }
        }

        merged.push(current);
        merged
    }

    #[allow(clippy::too_many_arguments)]
    async fn render_audio_clip_pitch_correction_with_rubberband<F>(
        src_path: &Path,
        session_root: &Path,
        clip_name: &str,
        offset: usize,
        length: usize,
        points: &[PitchCorrectionPoint],
        inertia_ms: u16,
        formant_compensation: bool,
        mut progress_callback: F,
    ) -> io::Result<(String, usize, crate::state::ClipPeaks)>
    where
        F: FnMut(f32, Option<String>),
    {
        let (samples, channels, sample_rate) =
            Self::decode_audio_to_f32_interleaved_with_progress(src_path, |decode_progress| {
                progress_callback(decode_progress * 0.45, Some("Decoding".to_string()));
            })
            .await?;
        let channels = channels.max(1);
        let total_frames = samples.len() / channels;
        let start_frame = offset.min(total_frames);
        let segment_frames = length.max(1).min(total_frames.saturating_sub(start_frame));
        if segment_frames == 0 {
            return Err(io::Error::other(format!(
                "Audio clip '{}' has no available source samples for pitch correction",
                clip_name
            )));
        }
        let start_idx = start_frame * channels;
        let end_idx = start_idx + segment_frames * channels;
        let segment_samples = &samples[start_idx..end_idx];
        progress_callback(0.50, Some("Preparing source".to_string()));
        tokio::task::yield_now().await;

        let stem = Path::new(clip_name)
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("audio");
        let temp_rel =
            Self::unique_import_rel_path(session_root, "audio", &format!("{stem}_src"), "wav")?;
        let temp_path = session_root.join(&temp_rel);
        let pitchmap_rel = Self::unique_import_rel_path(
            session_root,
            "audio",
            &format!("{stem}_pitchmap"),
            "txt",
        )?;
        let pitchmap_path = session_root.join(&pitchmap_rel);
        let output_rel = Self::unique_import_rel_path(
            session_root,
            "audio",
            &format!("{stem}_pitch_corrected"),
            "wav",
        )?;
        let output_path = session_root.join(&output_rel);

        {
            let spec = WavSpec {
                channels: channels as u16,
                sample_rate,
                bits_per_sample: 32,
                sample_format: SampleFormat::Float,
            };
            let mut writer = hound::WavWriter::create(&temp_path, spec).map_err(|e| {
                io::Error::other(format!(
                    "Failed to create WAV writer for '{}': {e}",
                    temp_path.display()
                ))
            })?;
            for sample in segment_samples {
                writer.write_sample(*sample).map_err(|e| {
                    io::Error::other(format!(
                        "Failed to write sample to '{}': {e}",
                        temp_path.display()
                    ))
                })?;
            }
            writer.finalize().map_err(|e| {
                io::Error::other(format!(
                    "Failed to finalize WAV '{}': {e}",
                    temp_path.display()
                ))
            })?;
        }

        let mut pitchmap = String::new();
        if points.is_empty() {
            pitchmap.push_str("0 0\n");
        } else {
            let mut sorted_points = points.to_vec();
            sorted_points.sort_by_key(|point| point.start_sample);
            let inertia_frames = ((sample_rate as u64 * inertia_ms as u64) / 1000) as usize;
            let mut previous_shift =
                sorted_points[0].target_midi_pitch - sorted_points[0].detected_midi_pitch;
            if sorted_points[0].start_sample > 0 {
                pitchmap.push_str(&format!("0 {:.6}\n", previous_shift));
            }
            for point in sorted_points {
                let start_sample = point.start_sample.min(segment_frames.saturating_sub(1));
                let target_shift = point.target_midi_pitch - point.detected_midi_pitch;
                if inertia_frames == 0 || (target_shift - previous_shift).abs() <= f32::EPSILON {
                    pitchmap.push_str(&format!("{start_sample} {:.6}\n", target_shift));
                } else {
                    pitchmap.push_str(&format!("{start_sample} {:.6}\n", previous_shift));
                    let glide_end = start_sample
                        .saturating_add(inertia_frames)
                        .min(segment_frames.saturating_sub(1));
                    if glide_end > start_sample {
                        pitchmap.push_str(&format!("{glide_end} {:.6}\n", target_shift));
                    } else {
                        pitchmap.push_str(&format!("{start_sample} {:.6}\n", target_shift));
                    }
                }
                previous_shift = target_shift;
            }
        }
        fs::write(&pitchmap_path, pitchmap).map_err(|e| {
            io::Error::other(format!(
                "Failed to write pitch map '{}': {e}",
                pitchmap_path.display()
            ))
        })?;
        progress_callback(0.60, Some("Writing pitch map".to_string()));
        tokio::task::yield_now().await;

        let command_result = tokio::process::Command::new("rubberband")
            .args({
                let mut args = vec![
                    "--quiet".to_string(),
                    "--fine".to_string(),
                    "--pitch-hq".to_string(),
                    "--pitch".to_string(),
                    "0".to_string(),
                    "--pitchmap".to_string(),
                    pitchmap_path.to_string_lossy().to_string(),
                    temp_path.to_string_lossy().to_string(),
                    output_path.to_string_lossy().to_string(),
                ];
                if formant_compensation {
                    args.insert(2, "--formant".to_string());
                }
                args
            })
            .output()
            .await;
        let _ = fs::remove_file(&temp_path);
        let _ = fs::remove_file(&pitchmap_path);
        progress_callback(0.90, Some("Rendering".to_string()));
        tokio::task::yield_now().await;

        let output = command_result
            .map_err(|e| io::Error::other(format!("Failed to launch Rubber Band: {e}")))?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
            let _ = fs::remove_file(&output_path);
            return Err(io::Error::other(if stderr.is_empty() {
                "Rubber Band failed to render pitch correction".to_string()
            } else {
                format!("Rubber Band failed: {stderr}")
            }));
        }
        let output_frames = Self::audio_clip_source_length(&output_path)?;
        progress_callback(0.95, Some("Calculating peaks".to_string()));
        tokio::task::yield_now().await;
        let peaks = Self::compute_audio_clip_peaks(&output_path)?;
        progress_callback(1.0, Some("Complete".to_string()));
        Ok((output_rel, output_frames.max(1), peaks))
    }

    fn write_wav_with_bit_depth(
        export_path: &Path,
        mixed_buffer: &[f32],
        sample_rate: i32,
        output_channels: usize,
        bit_depth: ExportBitDepth,
    ) -> io::Result<()> {
        let spec = WavSpec {
            channels: output_channels as u16,
            sample_rate: sample_rate as u32,
            bits_per_sample: match bit_depth {
                ExportBitDepth::Int16 => 16,
                ExportBitDepth::Int24 => 24,
                ExportBitDepth::Int32 => 32,
                ExportBitDepth::Float32 => 32,
            },
            sample_format: match bit_depth {
                ExportBitDepth::Float32 => SampleFormat::Float,
                _ => SampleFormat::Int,
            },
        };
        let mut writer = hound::WavWriter::create(export_path, spec)
            .map_err(|e| io::Error::other(format!("Failed to create WAV writer: {e}")))?;
        match bit_depth {
            ExportBitDepth::Int16 | ExportBitDepth::Int24 | ExportBitDepth::Int32 => {
                let (quantized, _) = Self::quantize_samples_for_bit_depth(mixed_buffer, bit_depth);
                for &q in &quantized {
                    match bit_depth {
                        ExportBitDepth::Int16 => writer.write_sample(q as i16),
                        _ => writer.write_sample(q),
                    }
                    .map_err(|e| io::Error::other(format!("Failed to write sample: {e}")))?;
                }
            }
            ExportBitDepth::Float32 => {
                for &sample in mixed_buffer {
                    writer
                        .write_sample(sample)
                        .map_err(|e| io::Error::other(format!("Failed to write sample: {e}")))?;
                }
            }
        }
        writer.finalize().map_err(|e| {
            io::Error::other(format!(
                "Failed to write '{}': {}",
                export_path.display(),
                e
            ))
        })
    }

    fn quantize_samples_for_bit_depth(
        mixed_buffer: &[f32],
        bit_depth: ExportBitDepth,
    ) -> (Vec<i32>, u8) {
        let (scale, min, max, bps) = match bit_depth {
            ExportBitDepth::Int16 => (i16::MAX as f32, i16::MIN as f32, i16::MAX as f32, 16),
            ExportBitDepth::Int24 => (8_388_607.0_f32, -8_388_608.0_f32, 8_388_607.0_f32, 24),
            ExportBitDepth::Int32 => (i32::MAX as f32, i32::MIN as f32, i32::MAX as f32, 32),
            ExportBitDepth::Float32 => (8_388_607.0_f32, -8_388_608.0_f32, 8_388_607.0_f32, 24),
        };

        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
        {
            use wide::{f32x4, i32x4};
            let mut quantized = Vec::with_capacity(mixed_buffer.len());
            let n = mixed_buffer.len() / 4;
            let vmin_f = f32x4::splat(min);
            let vmax_f = f32x4::splat(max);
            let scale_f = f32x4::splat(scale);
            let vmin_i = i32x4::splat(min as i32);
            let vmax_i = i32x4::splat(max as i32);
            for i in 0..n {
                let chunk = &mixed_buffer[i * 4..(i + 1) * 4];
                let v: f32x4 = [chunk[0], chunk[1], chunk[2], chunk[3]].into();
                let clamped = v.clamp(vmin_f, vmax_f);
                let scaled = clamped * scale_f;
                let rounded = scaled.round_int();
                let clamped_i = rounded.max(vmin_i).min(vmax_i);
                quantized.extend_from_slice(&clamped_i.to_array());
            }
            for s in &mixed_buffer[n * 4..] {
                quantized.push(
                    (*s).clamp(-1.0, 1.0)
                        .mul_add(scale, 0.0)
                        .round()
                        .clamp(min, max) as i32,
                );
            }
            (quantized, bps)
        }
        #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
        {
            let samples = mixed_buffer
                .iter()
                .map(|s| (s.clamp(-1.0, 1.0) * scale).round().clamp(min, max) as i32)
                .collect();
            (samples, bps)
        }
    }

    fn write_flac_with_bit_depth(
        export_path: &Path,
        mixed_buffer: &[f32],
        sample_rate: i32,
        output_channels: usize,
        bit_depth: ExportBitDepth,
    ) -> io::Result<()> {
        let (quantized, bits_per_sample) =
            Self::quantize_samples_for_bit_depth(mixed_buffer, bit_depth);
        let config = flacenc::config::Encoder::default()
            .into_verified()
            .map_err(|e| io::Error::other(format!("Invalid FLAC encoder config: {e:?}")))?;
        let source = flacenc::source::MemSource::from_samples(
            &quantized,
            output_channels,
            bits_per_sample as usize,
            sample_rate.max(1) as usize,
        );
        let stream = flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
            .map_err(|e| io::Error::other(format!("FLAC encode failed: {e}")))?;
        let mut sink = flacenc::bitsink::ByteSink::new();
        stream
            .write(&mut sink)
            .map_err(|e| io::Error::other(format!("FLAC bitstream write failed: {e}")))?;
        fs::write(export_path, sink.as_slice()).map_err(|e| {
            io::Error::other(format!(
                "Failed to write '{}': {}",
                export_path.display(),
                e
            ))
        })
    }

    fn ffmpeg_init() -> Result<(), ffmpeg_next::Error> {
        static RESULT: std::sync::OnceLock<Result<(), ffmpeg_next::Error>> =
            std::sync::OnceLock::new();
        *RESULT.get_or_init(ffmpeg_next::init)
    }

    fn write_mp3(
        export_path: &Path,
        mixed_buffer: &[f32],
        sample_rate: i32,
        output_channels: usize,
        codec: ExportCodecSettings,
        metadata: &ExportMetadata,
    ) -> io::Result<()> {
        if output_channels != 1 && output_channels != 2 {
            return Err(io::Error::other(format!(
                "MP3 export supports only mono/stereo, got {} channels",
                output_channels
            )));
        }

        Self::ffmpeg_init().map_err(|e| io::Error::other(format!("FFmpeg init failed: {e}")))?;

        let mut octx = output(export_path.to_str().unwrap_or("output.mp3"))
            .map_err(|e| io::Error::other(format!("Failed to create output context: {e}")))?;

        let codec_id = CodecId::MP3;
        let encoder_codec = ffmpeg_next::codec::encoder::find(codec_id)
            .ok_or_else(|| io::Error::other("MP3 encoder not found"))?;

        let mut encoder = CodecContext::new_with_codec(encoder_codec)
            .encoder()
            .audio()
            .map_err(|e| io::Error::other(format!("Failed to create audio encoder: {e}")))?;

        encoder.set_rate(sample_rate);

        encoder.set_format(ffmpeg_next::format::Sample::F32(
            ffmpeg_next::format::sample::Type::Planar,
        ));
        encoder.set_channel_layout(match output_channels {
            1 => ffmpeg_next::channel_layout::ChannelLayout::MONO,
            _ => ffmpeg_next::channel_layout::ChannelLayout::STEREO,
        });

        let bitrate = (codec.mp3_bitrate_kbps as usize) * 1000;
        encoder.set_bit_rate(bitrate);

        if matches!(codec.mp3_mode, ExportMp3Mode::Vbr) {
            encoder.set_quality(2usize);
        }

        let mut metadata_dict = Dictionary::new();
        if !metadata.author.is_empty() {
            metadata_dict.set("artist", &metadata.author);
        }
        if !metadata.album.is_empty() {
            metadata_dict.set("album", &metadata.album);
        }
        if let Some(year) = metadata.year {
            metadata_dict.set("date", &year.to_string());
        }
        if let Some(track_number) = metadata.track_number {
            metadata_dict.set("track", &track_number.to_string());
        }
        if !metadata.genre.is_empty() {
            metadata_dict.set("genre", &metadata.genre);
        }

        let mut output_stream = octx
            .add_stream(encoder_codec)
            .map_err(|e| io::Error::other(format!("Failed to add stream: {e}")))?;

        output_stream.set_parameters(&encoder);

        let mut encoder = encoder
            .open_as(encoder_codec)
            .map_err(|e| io::Error::other(format!("Failed to open encoder: {e}")))?;

        octx.write_header()
            .map_err(|e| io::Error::other(format!("Failed to write header: {e}")))?;

        let frame_size = 1152;

        for chunk_start in (0..mixed_buffer.len()).step_by(frame_size * output_channels) {
            let chunk_end = (chunk_start + frame_size * output_channels).min(mixed_buffer.len());
            let chunk = &mixed_buffer[chunk_start..chunk_end];
            let actual_frames = chunk.len() / output_channels;

            if actual_frames == 0 {
                continue;
            }

            let mut frame = Audio::empty();
            frame.set_format(encoder.format());
            frame.set_channel_layout(encoder.channel_layout());
            frame.set_rate(encoder.rate());
            frame.set_samples(actual_frames);

            unsafe {
                ffmpeg_next::ffi::av_frame_get_buffer(frame.as_mut_ptr(), 0);
            }

            for ch in 0..output_channels {
                let data_ptr = frame.data_mut(ch).as_mut_ptr() as *mut f32;
                for frame_idx in 0..actual_frames {
                    let src_idx = frame_idx * output_channels + ch;
                    if src_idx < chunk.len() {
                        unsafe {
                            *data_ptr.add(frame_idx) = chunk[src_idx];
                        }
                    }
                }
            }

            encoder
                .send_frame(&frame)
                .map_err(|e| io::Error::other(format!("Failed to send frame: {e}")))?;

            let mut packet = ffmpeg_next::packet::Packet::empty();
            while encoder.receive_packet(&mut packet).is_ok() {
                packet.set_stream(0);
                packet
                    .write_interleaved(&mut octx)
                    .map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
            }
        }

        encoder
            .send_eof()
            .map_err(|e| io::Error::other(format!("Failed to send EOF: {e}")))?;

        let mut packet = ffmpeg_next::packet::Packet::empty();
        while encoder.receive_packet(&mut packet).is_ok() {
            packet.set_stream(0);
            packet
                .write_interleaved(&mut octx)
                .map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
        }

        octx.write_trailer()
            .map_err(|e| io::Error::other(format!("Failed to write trailer: {e}")))?;

        Ok(())
    }

    fn write_ogg_vorbis(
        export_path: &Path,
        mixed_buffer: &[f32],
        sample_rate: i32,
        output_channels: usize,
        codec: ExportCodecSettings,
        metadata: &ExportMetadata,
    ) -> io::Result<()> {
        Self::ffmpeg_init().map_err(|e| io::Error::other(format!("FFmpeg init failed: {e}")))?;

        let mut octx = output(export_path.to_str().unwrap_or("output.ogg"))
            .map_err(|e| io::Error::other(format!("Failed to create output context: {e}")))?;

        let codec_id = CodecId::VORBIS;
        let encoder_codec = ffmpeg_next::codec::encoder::find(codec_id)
            .ok_or_else(|| io::Error::other("Vorbis encoder not found"))?;

        let mut encoder = CodecContext::new_with_codec(encoder_codec)
            .encoder()
            .audio()
            .map_err(|e| io::Error::other(format!("Failed to create audio encoder: {e}")))?;

        encoder.set_rate(sample_rate);

        encoder.set_format(ffmpeg_next::format::Sample::F32(
            ffmpeg_next::format::sample::Type::Planar,
        ));
        encoder.set_channel_layout(match output_channels {
            1 => ffmpeg_next::channel_layout::ChannelLayout::MONO,
            _ => ffmpeg_next::channel_layout::ChannelLayout::STEREO,
        });

        let quality = ((codec.ogg_quality + 0.1) * 10.0).clamp(0.0, 10.0) as i32;
        encoder.set_quality(quality as usize);

        let mut metadata_dict = Dictionary::new();
        if !metadata.author.is_empty() {
            metadata_dict.set("artist", &metadata.author);
        }
        if !metadata.album.is_empty() {
            metadata_dict.set("album", &metadata.album);
        }
        if let Some(year) = metadata.year {
            metadata_dict.set("date", &year.to_string());
        }
        if let Some(track_number) = metadata.track_number {
            metadata_dict.set("track", &track_number.to_string());
        }
        if !metadata.genre.is_empty() {
            metadata_dict.set("genre", &metadata.genre);
        }

        let mut output_stream = octx
            .add_stream(encoder_codec)
            .map_err(|e| io::Error::other(format!("Failed to add stream: {e}")))?;

        output_stream.set_parameters(&encoder);

        let mut encoder = encoder
            .open_as(encoder_codec)
            .map_err(|e| io::Error::other(format!("Failed to open encoder: {e}")))?;

        octx.write_header()
            .map_err(|e| io::Error::other(format!("Failed to write header: {e}")))?;

        let frame_size = 1024;

        for chunk_start in (0..mixed_buffer.len()).step_by(frame_size * output_channels) {
            let chunk_end = (chunk_start + frame_size * output_channels).min(mixed_buffer.len());
            let chunk = &mixed_buffer[chunk_start..chunk_end];
            let actual_frames = chunk.len() / output_channels;

            if actual_frames == 0 {
                continue;
            }

            let mut frame = Audio::empty();
            frame.set_format(encoder.format());
            frame.set_channel_layout(encoder.channel_layout());
            frame.set_rate(encoder.rate());
            frame.set_samples(actual_frames);

            unsafe {
                ffmpeg_next::ffi::av_frame_get_buffer(frame.as_mut_ptr(), 0);
            }

            for ch in 0..output_channels {
                let data_ptr = frame.data_mut(ch).as_mut_ptr() as *mut f32;
                for frame_idx in 0..actual_frames {
                    let src_idx = frame_idx * output_channels + ch;
                    if src_idx < chunk.len() {
                        unsafe {
                            *data_ptr.add(frame_idx) = chunk[src_idx];
                        }
                    }
                }
            }

            encoder
                .send_frame(&frame)
                .map_err(|e| io::Error::other(format!("Failed to send frame: {e}")))?;

            let mut packet = ffmpeg_next::packet::Packet::empty();
            while encoder.receive_packet(&mut packet).is_ok() {
                packet.set_stream(0);
                packet
                    .write_interleaved(&mut octx)
                    .map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
            }
        }

        encoder
            .send_eof()
            .map_err(|e| io::Error::other(format!("Failed to send EOF: {e}")))?;

        let mut packet = ffmpeg_next::packet::Packet::empty();
        while encoder.receive_packet(&mut packet).is_ok() {
            packet.set_stream(0);
            packet
                .write_interleaved(&mut octx)
                .map_err(|e| io::Error::other(format!("Failed to write packet: {e}")))?;
        }

        octx.write_trailer()
            .map_err(|e| io::Error::other(format!("Failed to write trailer: {e}")))?;

        Ok(())
    }

    fn write_export_audio(req: ExportWriteRequest<'_>) -> io::Result<()> {
        let export_path = req.export_path;
        let tmp_path = export_path.with_extension(format!(
            "{}.tmp",
            export_path
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or("export")
        ));
        let result = match req.format {
            ExportFormat::Wav => Self::write_wav_with_bit_depth(
                &tmp_path,
                req.mixed_buffer,
                req.sample_rate,
                req.output_channels,
                req.bit_depth,
            ),
            ExportFormat::Flac => Self::write_flac_with_bit_depth(
                &tmp_path,
                req.mixed_buffer,
                req.sample_rate,
                req.output_channels,
                req.bit_depth,
            ),
            ExportFormat::Mp3 => Self::write_mp3(
                &tmp_path,
                req.mixed_buffer,
                req.sample_rate,
                req.output_channels,
                req.codec,
                req.metadata,
            ),
            ExportFormat::Ogg => Self::write_ogg_vorbis(
                &tmp_path,
                req.mixed_buffer,
                req.sample_rate,
                req.output_channels,
                req.codec,
                req.metadata,
            ),
        };
        if result.is_ok() {
            fs::rename(&tmp_path, export_path)?;
        }
        result
    }

    fn measure_lufs_and_true_peak(
        samples: &[f32],
        channels: usize,
        sample_rate: i32,
    ) -> io::Result<(f32, f32)> {
        let mut meter = EbuR128::new(
            channels as u32,
            sample_rate as u32,
            LoudnessMode::I | LoudnessMode::TRUE_PEAK,
        )
        .map_err(|e| io::Error::other(format!("Failed to initialize loudness meter: {e}")))?;
        meter
            .add_frames_f32(samples)
            .map_err(|e| io::Error::other(format!("Loudness analysis failed: {e}")))?;
        let lufs = meter
            .loudness_global()
            .map_err(|e| io::Error::other(format!("Failed to get integrated loudness: {e}")))?
            as f32;
        if !lufs.is_finite() {
            Err(io::Error::other("Integrated loudness is not finite"))
        } else {
            let mut tp = 0.0_f32;
            for ch in 0..channels as u32 {
                let p = meter
                    .true_peak(ch)
                    .map_err(|e| io::Error::other(format!("Failed to get true peak: {e}")))?
                    as f32;
                tp = tp.max(p);
            }
            Ok((lufs, tp))
        }
    }

    fn apply_export_normalization(
        mixed_buffer: &mut [f32],
        params: ExportNormalizeParams,
    ) -> io::Result<()> {
        match params.mode {
            ExportNormalizeMode::Peak => {
                let peak = maolan_engine::simd::peak_abs(mixed_buffer);
                if peak > 0.0 {
                    let target_amp = 10.0_f32.powf(params.target_dbfs / 20.0).clamp(0.0, 1.0);
                    let gain = target_amp / peak;
                    maolan_engine::simd::mul_inplace(mixed_buffer, gain);
                }
            }
            ExportNormalizeMode::Loudness => {
                let (measured_lufs, measured_tp_amp) = Self::measure_lufs_and_true_peak(
                    mixed_buffer,
                    params.output_channels,
                    params.sample_rate,
                )?;
                let gain_loudness_db = params.target_lufs - measured_lufs;
                let gain_loudness = 10.0_f32.powf(gain_loudness_db / 20.0);

                let ceiling_amp = 10.0_f32.powf(params.true_peak_dbtp / 20.0).clamp(0.0, 1.0);
                let gain_tp = if measured_tp_amp > 0.0 {
                    ceiling_amp / measured_tp_amp
                } else {
                    gain_loudness
                };

                let applied_gain = if params.tp_limiter {
                    gain_loudness
                } else {
                    gain_loudness.min(gain_tp)
                };

                maolan_engine::simd::mul_inplace(mixed_buffer, applied_gain);

                if params.tp_limiter {
                    let predicted_tp = measured_tp_amp * applied_gain;
                    if predicted_tp > ceiling_amp && ceiling_amp > 0.0 {
                        maolan_engine::simd::clamp_inplace(mixed_buffer, -ceiling_amp, ceiling_amp);
                    }
                }
            }
        }
        Ok(())
    }

    fn available_export_hw_out_ports(&self) -> Vec<usize> {
        let channels = self
            .state
            .blocking_read()
            .hw_out
            .as_ref()
            .map(|hw| hw.channels)
            .unwrap_or(0);
        (0..channels).collect()
    }

    fn default_export_hw_out_ports(&self) -> BTreeSet<usize> {
        self.available_export_hw_out_ports()
            .into_iter()
            .take(2)
            .collect()
    }

    fn normalize_export_hw_out_ports(&mut self) {
        let available: BTreeSet<usize> = self.available_export_hw_out_ports().into_iter().collect();
        self.export_hw_out_ports
            .retain(|port| available.contains(port));
        if self.export_hw_out_ports.is_empty() {
            self.export_hw_out_ports = self.default_export_hw_out_ports();
        }
    }

    fn export_max_channels_for_current_settings(&self) -> usize {
        if matches!(self.export_render_mode, ExportRenderMode::Mixdown) {
            return self.export_hw_out_ports.len();
        }

        let state = self.state.blocking_read();
        state
            .tracks
            .iter()
            .filter(|track| state.selected.contains(&track.name))
            .map(|track| track.audio.outs.max(1))
            .max()
            .unwrap_or(0)
    }

    fn export_mp3_supported_for_current_settings(&self) -> bool {
        self.export_max_channels_for_current_settings() <= 2
    }

    fn mix_track_clips_to_channels(
        clips: &[crate::state::AudioClip],
        session_root: &Path,
        total_length: usize,
        output_channels: usize,
        level_db: f32,
        balance: f32,
        apply_fader: bool,
    ) -> io::Result<Vec<f32>> {
        let output_channels = output_channels.max(1);
        let mut mixed = vec![0.0_f32; total_length * output_channels];
        let channel_gains = if apply_fader {
            let level_amp = 10.0_f32.powf(level_db / 20.0);
            if output_channels == 2 {
                vec![
                    if balance <= 0.0 {
                        level_amp
                    } else {
                        level_amp * (1.0 - balance)
                    },
                    if balance >= 0.0 {
                        level_amp
                    } else {
                        level_amp * (1.0 + balance)
                    },
                ]
            } else {
                vec![level_amp; output_channels]
            }
        } else {
            vec![1.0; output_channels]
        };

        for clip in clips {
            let clip_path = if std::path::PathBuf::from(&clip.name).is_absolute() {
                std::path::PathBuf::from(&clip.name)
            } else {
                session_root.join(&clip.name)
            };
            let mut reader = WavReader::open(&clip_path).map_err(|e| {
                io::Error::other(format!(
                    "Failed to open WAV '{}': {}",
                    clip_path.display(),
                    e
                ))
            })?;
            let spec = reader.spec();
            let clip_channels = spec.channels.max(1) as usize;
            let samples: Vec<f32> = match spec.sample_format {
                SampleFormat::Float => reader.samples::<f32>().filter_map(|s| s.ok()).collect(),
                SampleFormat::Int => match spec.bits_per_sample {
                    16 => reader
                        .samples::<i16>()
                        .filter_map(|s| s.ok())
                        .map(|s| s as f32 * (1.0 / 32768.0))
                        .collect(),
                    24 => reader
                        .samples::<i32>()
                        .filter_map(|s| s.ok())
                        .map(|s| s as f32 * (1.0 / 8_388_608.0))
                        .collect(),
                    32 => reader
                        .samples::<i32>()
                        .filter_map(|s| s.ok())
                        .map(|s| s as f32 * (1.0 / 2_147_483_648.0))
                        .collect(),
                    _ => continue,
                },
            };
            if samples.is_empty() {
                continue;
            }
            let clip_frames = samples.len() / clip_channels;
            let start_frame = clip.start;
            let offset_frame = clip.offset;
            let length_frames = clip.length.min(clip_frames.saturating_sub(offset_frame));

            for frame_idx in 0..length_frames {
                let src_frame = offset_frame + frame_idx;
                let dst_frame = start_frame + frame_idx;
                if dst_frame >= total_length {
                    break;
                }
                let src_idx = src_frame * clip_channels;
                if src_idx + clip_channels > samples.len() {
                    break;
                }
                let dst_idx = dst_frame * output_channels;
                for out_ch in 0..output_channels {
                    let source_sample = if clip_channels == 1 {
                        samples[src_idx]
                    } else {
                        let source_ch = out_ch.min(clip_channels.saturating_sub(1));
                        samples[src_idx + source_ch]
                    };
                    mixed[dst_idx + out_ch] += source_sample * channel_gains[out_ch];
                }
            }
        }

        Ok(mixed)
    }

    async fn export_session<F>(
        options: &ExportSessionOptions,
        cancel: &Arc<AtomicBool>,
        bounce_notify: Option<Arc<tokio::sync::Notify>>,
        mut progress_callback: F,
    ) -> std::io::Result<()>
    where
        F: FnMut(f32, Option<String>),
    {
        let export_path = options.export_path.as_path();
        let sample_rate = options.sample_rate;
        let export_formats = options.formats.clone();
        let bit_depth = options.bit_depth;
        let mp3_mode = options.mp3_mode;
        let mp3_bitrate_kbps = options.mp3_bitrate_kbps;
        let ogg_quality = options.ogg_quality;
        let realtime_fallback = options.realtime_fallback;
        let normalize = options.normalize;
        let normalize_target_dbfs = options.normalize_target_dbfs;
        let normalize_mode = options.normalize_mode;
        let normalize_target_lufs = options.normalize_target_lufs;
        let normalize_true_peak_dbtp = options.normalize_true_peak_dbtp;
        let normalize_tp_limiter = options.normalize_tp_limiter;
        let master_limiter = options.master_limiter;
        let master_limiter_ceiling_dbtp = options.master_limiter_ceiling_dbtp;
        let metadata_author = options.metadata_author.clone();
        let metadata_album = options.metadata_album.clone();
        let metadata_year = options.metadata_year;
        let metadata_track_number = options.metadata_track_number;
        let metadata_genre = options.metadata_genre.clone();
        let codec = ExportCodecSettings {
            mp3_mode,
            mp3_bitrate_kbps,
            ogg_quality,
        };
        let metadata = ExportMetadata {
            author: metadata_author,
            album: metadata_album,
            year: metadata_year,
            track_number: metadata_track_number,
            genre: metadata_genre,
        };
        let render_mode = options.render_mode;
        let selected_hw_out_ports = options.selected_hw_out_ports.clone();
        let state = options.state.clone();
        let session_root = options.session_root.as_path();

        progress_callback(0.0, Some("Analyzing tracks".to_string()));
        tokio::task::yield_now().await;

        let (mut tracks, connections, total_length, selected_tracks) = {
            let state = state.read().await;
            let mut max_length = 0_usize;
            let tracks_data: Vec<_> = state
                .tracks
                .iter()
                .map(|track| {
                    let mut track_max = 0_usize;
                    for clip in &track.audio.clips {
                        let clip_end = clip.start + clip.length;
                        track_max = track_max.max(clip_end);
                    }
                    max_length = max_length.max(track_max);
                    ExportTrackData {
                        name: track.name.clone(),
                        level: track.level,
                        balance: track.balance,
                        muted: track.muted,
                        soloed: track.soloed,
                        output_ports: track.audio.outs.max(1),
                        clips: track.audio.clips.clone(),
                    }
                })
                .collect();
            (
                tracks_data,
                state.connections.clone(),
                max_length,
                state.selected.iter().cloned().collect::<HashSet<String>>(),
            )
        };

        {
            let mut handles = Vec::new();
            for (track_idx, track) in tracks.iter().enumerate() {
                for (clip_idx, clip) in track.clips.iter().enumerate() {
                    if clip.pitch_correction_points.is_empty() {
                        continue;
                    }
                    let source_name = clip
                        .pitch_correction_source_name
                        .clone()
                        .unwrap_or_else(|| clip.name.clone());
                    let source_path = if std::path::PathBuf::from(&source_name).is_absolute() {
                        std::path::PathBuf::from(&source_name)
                    } else {
                        session_root.join(&source_name)
                    };
                    let clip_name = clip.name.clone();
                    let offset = clip.pitch_correction_source_offset.unwrap_or(clip.offset);
                    let length = clip.pitch_correction_source_length.unwrap_or(clip.length);
                    let points = clip.pitch_correction_points.clone();
                    let inertia_ms = clip.pitch_correction_inertia_ms.unwrap_or(100);
                    let formant = clip.pitch_correction_formant_compensation.unwrap_or(true);
                    let session_root = session_root.to_path_buf();
                    handles.push(tokio::spawn(async move {
                        let result = Maolan::render_audio_clip_pitch_correction_with_rubberband(
                            &source_path,
                            &session_root,
                            &clip_name,
                            offset,
                            length,
                            &points,
                            inertia_ms,
                            formant,
                            |_, _| {},
                        )
                        .await;
                        (track_idx, clip_idx, result)
                    }));
                }
            }
            if !handles.is_empty() {
                progress_callback(0.02, Some("Rendering pitch correction...".to_string()));
                tokio::task::yield_now().await;
                for handle in handles {
                    if cancel.load(std::sync::atomic::Ordering::Relaxed) {
                        return Err(io::Error::other("Export cancelled".to_string()));
                    }
                    let (track_idx, clip_idx, result) = handle.await.map_err(|e| {
                        io::Error::other(format!("Pitch correction task panicked: {e}"))
                    })?;
                    let (rendered_name, rendered_length, _) = result
                        .map_err(|e| io::Error::other(format!("Pitch correction failed: {e}")))?;
                    tracks[track_idx].clips[clip_idx].name = rendered_name;
                    tracks[track_idx].clips[clip_idx].offset = 0;
                    tracks[track_idx].clips[clip_idx].length = rendered_length.max(1);
                    tracks[track_idx].clips[clip_idx]
                        .pitch_correction_points
                        .clear();
                }
            }
        }

        let stem_mode_label = if matches!(render_mode, ExportRenderMode::StemsPreFader) {
            "pre"
        } else {
            "post"
        };
        let export_parent = export_path.parent().unwrap_or_else(|| Path::new("."));
        let export_stem = export_path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("export");
        let stem_dir = export_parent.join(format!("{export_stem}_stems"));

        let mut temp_bounce_files: Vec<PathBuf> = Vec::new();
        if matches!(render_mode, ExportRenderMode::StemsPostFader) {
            let has_solo = tracks.iter().any(|track| track.soloed);
            let tracks_to_bounce: Vec<usize> = tracks
                .iter()
                .enumerate()
                .filter(|(_, track)| {
                    selected_tracks.contains(&track.name)
                        && !track.muted
                        && (!has_solo || track.soloed)
                })
                .map(|(idx, _)| idx)
                .collect();

            if !tracks_to_bounce.is_empty() {
                fs::create_dir_all(&stem_dir).map_err(|e| {
                    io::Error::other(format!(
                        "Failed to create stem directory '{}': {}",
                        stem_dir.display(),
                        e
                    ))
                })?;

                struct BounceInfo {
                    track_idx: usize,
                    temp_path: PathBuf,
                    render_length: usize,
                    automation_lanes: Vec<OfflineAutomationLane>,
                }

                let mut bounce_infos: Vec<BounceInfo> = Vec::new();
                for track_idx in &tracks_to_bounce {
                    let track = &tracks[*track_idx];
                    let render_length = track
                        .clips
                        .iter()
                        .map(|clip| clip.start.saturating_add(clip.length))
                        .max()
                        .unwrap_or(0)
                        .max(1);
                    let automation_lanes = {
                        let state_guard = state.read().await;
                        if let Some(t) = state_guard.tracks.iter().find(|t| t.name == track.name) {
                            build_offline_automation_lanes(&t.automation_lanes)
                        } else {
                            Vec::new()
                        }
                    };
                    let temp_path = stem_dir.join(format!(
                        ".maolan_bounce_{}_{}.wav",
                        std::process::id(),
                        Self::sanitize_export_component(&track.name)
                    ));
                    temp_bounce_files.push(temp_path.clone());
                    bounce_infos.push(BounceInfo {
                        track_idx: *track_idx,
                        temp_path,
                        render_length,
                        automation_lanes,
                    });
                }

                progress_callback(0.05, Some("Bouncing tracks...".to_string()));
                tokio::task::yield_now().await;
                for info in &bounce_infos {
                    if cancel.load(std::sync::atomic::Ordering::Relaxed) {
                        return Err(io::Error::other("Export cancelled".to_string()));
                    }
                    if let Err(e) = CLIENT
                        .send(EngineMessage::Request(Action::TrackOfflineBounce {
                            track_name: tracks[info.track_idx].name.clone(),
                            output_path: info.temp_path.to_string_lossy().to_string(),
                            start_sample: 0,
                            length_samples: info.render_length,
                            automation_lanes: info.automation_lanes.clone(),
                            apply_fader: true,
                        }))
                        .await
                    {
                        return Err(io::Error::other(format!(
                            "Failed to request bounce for '{}': {e}",
                            tracks[info.track_idx].name
                        )));
                    }
                }

                if let Some(notify) = bounce_notify {
                    notify.notified().await;
                    progress_callback(0.20, Some("Bounces complete".to_string()));
                    tokio::task::yield_now().await;
                }

                for info in &bounce_infos {
                    let bounced_clip = AudioClip {
                        name: info.temp_path.to_string_lossy().to_string(),
                        start: 0,
                        length: info.render_length,
                        offset: 0,
                        input_channel: 0,
                        muted: false,
                        max_length_samples: info.render_length,
                        source_length_samples: info.render_length,
                        peaks_file: None,
                        peaks: ClipPeaks::default(),
                        fade_enabled: false,
                        fade_in_samples: 0,
                        fade_out_samples: 0,
                        pitch_correction_preview_name: None,
                        pitch_correction_source_name: None,
                        pitch_correction_source_offset: None,
                        pitch_correction_source_length: None,
                        pitch_correction_points: vec![],
                        pitch_correction_frame_likeness: None,
                        pitch_correction_inertia_ms: None,
                        pitch_correction_formant_compensation: None,
                        ..Default::default()
                    };
                    tracks[info.track_idx].clips = vec![bounced_clip];
                }
            }
        }

        if total_length == 0 {
            for path in &temp_bounce_files {
                let _ = fs::remove_file(path);
            }
            return Err(io::Error::other(
                "No audio clips found. Nothing to export.".to_string(),
            ));
        }
        if export_formats.is_empty() {
            return Err(io::Error::other("Select at least one export format"));
        }

        let has_solo = tracks.iter().any(|track| track.soloed);
        if !matches!(render_mode, ExportRenderMode::Mixdown) && selected_tracks.is_empty() {
            return Err(io::Error::other(
                "Stem export requires at least one selected track",
            ));
        }
        progress_callback(0.1, Some("Loading and mixing tracks".to_string()));
        tokio::task::yield_now().await;
        if matches!(render_mode, ExportRenderMode::Mixdown) {
            let output_ports = if selected_hw_out_ports.is_empty() {
                let mut routed_ports: Vec<usize> = connections
                    .iter()
                    .filter(|conn| conn.kind == Kind::Audio && conn.to_track == "hw:out")
                    .map(|conn| conn.to_port)
                    .collect();
                routed_ports.sort_unstable();
                routed_ports.dedup();
                routed_ports
            } else {
                selected_hw_out_ports
            };
            if output_ports.is_empty() {
                return Err(io::Error::other(
                    "Mixdown export requires at least one selected hw:out port",
                ));
            }
            let output_channels = output_ports.len();
            let normalize_params = ExportNormalizeParams {
                mode: normalize_mode,
                target_dbfs: normalize_target_dbfs,
                target_lufs: normalize_target_lufs,
                true_peak_dbtp: normalize_true_peak_dbtp,
                tp_limiter: normalize_tp_limiter,
                sample_rate,
                output_channels,
            };
            let hw_out_channel_map: HashMap<usize, usize> = output_ports
                .iter()
                .enumerate()
                .map(|(channel_idx, port)| (*port, channel_idx))
                .collect();
            let mut mixed_buffer = vec![0.0_f32; total_length * output_channels];
            for (track_idx, track) in tracks.iter().enumerate() {
                if track.muted || (has_solo && !track.soloed) {
                    continue;
                }
                let track_progress_start = 0.1 + (track_idx as f32 / tracks.len() as f32) * 0.7;
                let track_progress_span = 0.7 / tracks.len() as f32;
                progress_callback(
                    track_progress_start,
                    Some(format!("Processing track: {}", track.name)),
                );
                tokio::task::yield_now().await;
                let routed_ports: Vec<(usize, usize)> = connections
                    .iter()
                    .filter(|conn| {
                        conn.kind == Kind::Audio
                            && conn.from_track == track.name
                            && conn.to_track == "hw:out"
                    })
                    .filter_map(|conn| {
                        hw_out_channel_map
                            .get(&conn.to_port)
                            .map(|dest_idx| (conn.from_port, *dest_idx))
                    })
                    .collect();
                if routed_ports.is_empty() {
                    continue;
                }
                let track_buffer = Self::mix_track_clips_to_channels(
                    &track.clips,
                    session_root,
                    total_length,
                    track.output_ports,
                    track.level,
                    track.balance,
                    true,
                )?;
                let valid_ports: Vec<(usize, usize)> = routed_ports
                    .into_iter()
                    .filter(|(s, _)| *s < track.output_ports)
                    .collect();
                let is_identity = output_channels == track.output_ports
                    && valid_ports.len() == output_channels
                    && valid_ports.iter().all(|(s, d)| s == d)
                    && valid_ports.iter().map(|(s, _)| *s).max()
                        == Some(output_channels.saturating_sub(1));
                if is_identity {
                    maolan_engine::simd::add_inplace(&mut mixed_buffer, &track_buffer);
                } else if valid_ports.len() == 1 {
                    let (source_port, dest_channel) = valid_ports[0];
                    let mut mixed_idx = dest_channel;
                    let mut track_idx = source_port;
                    for _ in 0..total_length {
                        mixed_buffer[mixed_idx] += track_buffer[track_idx];
                        mixed_idx += output_channels;
                        track_idx += track.output_ports;
                    }
                } else {
                    let is_stereo_swap = output_channels == 2
                        && track.output_ports == 2
                        && valid_ports.len() == 2
                        && valid_ports[0] == (0, 1)
                        && valid_ports[1] == (1, 0);
                    if is_stereo_swap {
                        #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
                        unsafe {
                            use std::arch::x86_64::*;
                            let mut frame = 0;
                            let end = total_length.saturating_sub(3);
                            while frame < end {
                                let t1 = _mm_loadu_ps(track_buffer.as_ptr().add(frame * 2));
                                let t2 = _mm_loadu_ps(track_buffer.as_ptr().add(frame * 2 + 4));
                                let shuffled1 = _mm_shuffle_ps(t1, t1, 0xB1);
                                let shuffled2 = _mm_shuffle_ps(t2, t2, 0xB1);
                                let m1 = _mm_loadu_ps(mixed_buffer.as_ptr().add(frame * 2));
                                let m2 = _mm_loadu_ps(mixed_buffer.as_ptr().add(frame * 2 + 4));
                                _mm_storeu_ps(
                                    mixed_buffer.as_mut_ptr().add(frame * 2),
                                    _mm_add_ps(m1, shuffled1),
                                );
                                _mm_storeu_ps(
                                    mixed_buffer.as_mut_ptr().add(frame * 2 + 4),
                                    _mm_add_ps(m2, shuffled2),
                                );
                                frame += 4;
                            }
                            for frame in frame..total_length {
                                let track_base = frame * 2;
                                let mixed_base = frame * 2;
                                mixed_buffer[mixed_base + 1] += track_buffer[track_base];
                                mixed_buffer[mixed_base] += track_buffer[track_base + 1];
                            }
                        }
                        #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
                        {
                            for frame in 0..total_length {
                                let track_base = frame * 2;
                                let mixed_base = frame * 2;
                                mixed_buffer[mixed_base + 1] += track_buffer[track_base];
                                mixed_buffer[mixed_base] += track_buffer[track_base + 1];
                            }
                        }
                    } else {
                        for (source_port, dest_channel) in &valid_ports {
                            let mut mixed_idx = *dest_channel;
                            let mut track_idx = *source_port;
                            for _ in 0..total_length {
                                mixed_buffer[mixed_idx] += track_buffer[track_idx];
                                mixed_idx += output_channels;
                                track_idx += track.output_ports;
                            }
                        }
                    }
                }
                progress_callback(
                    track_progress_start + track_progress_span,
                    Some(format!("Finished: {}", track.name)),
                );
                tokio::task::yield_now().await;
            }
            if realtime_fallback {
                progress_callback(0.82, Some("Real-time fallback pacing".to_string()));
                let seconds = (total_length as f64 / sample_rate.max(1) as f64).max(0.0);
                tokio::time::sleep(Duration::from_secs_f64(seconds)).await;
            }

            if normalize {
                Self::apply_export_normalization(&mut mixed_buffer, normalize_params)?;
            }
            if master_limiter {
                let ceiling_amp = 10.0_f32
                    .powf(master_limiter_ceiling_dbtp / 20.0)
                    .clamp(0.0, 1.0);
                maolan_engine::simd::clamp_inplace(&mut mixed_buffer, -ceiling_amp, ceiling_amp);
            }
            let base_path = Self::export_base_path(export_path.to_path_buf());
            let write_span = 0.1 / export_formats.len().max(1) as f32;
            for (format_idx, format) in export_formats.iter().enumerate() {
                let write_progress = 0.9 + write_span * format_idx as f32;
                progress_callback(
                    write_progress.clamp(0.0, 0.99),
                    Some(format!("Writing {} ({bit_depth})", format)),
                );
                tokio::task::yield_now().await;
                let out_path = base_path.with_extension(Self::export_format_extension(*format));
                Self::write_export_audio(ExportWriteRequest {
                    export_path: &out_path,
                    mixed_buffer: &mixed_buffer,
                    sample_rate,
                    output_channels,
                    bit_depth,
                    format: *format,
                    codec,
                    metadata: &metadata,
                })?;
            }
            progress_callback(1.0, Some("Complete".to_string()));

            for path in &temp_bounce_files {
                let _ = fs::remove_file(path);
            }
            return Ok(());
        }

        let selected_tracks: Vec<_> = tracks
            .iter()
            .filter(|track| {
                selected_tracks.contains(&track.name) && !track.muted && (!has_solo || track.soloed)
            })
            .collect();
        if selected_tracks.is_empty() {
            return Err(io::Error::other(
                "No selected tracks are eligible for stem export",
            ));
        }

        for (idx, track) in selected_tracks.iter().enumerate() {
            if cancel.load(std::sync::atomic::Ordering::Relaxed) {
                return Err(io::Error::other("Export cancelled".to_string()));
            }
            let start = 0.1 + (idx as f32 / selected_tracks.len() as f32) * 0.75;
            progress_callback(start, Some(format!("Rendering stem: {}", track.name)));
            tokio::task::yield_now().await;
            let output_channels = track.output_ports.max(1);

            let is_post_fader = matches!(render_mode, ExportRenderMode::StemsPostFader);
            let can_zero_copy = is_post_fader
                && export_formats.len() == 1
                && export_formats[0] == ExportFormat::Wav
                && bit_depth == ExportBitDepth::Float32
                && !normalize
                && !master_limiter;

            if can_zero_copy {
                let temp_path = track
                    .clips
                    .first()
                    .map(|c| {
                        let p = PathBuf::from(&c.name);
                        if p.is_absolute() {
                            p
                        } else {
                            session_root.join(&c.name)
                        }
                    })
                    .ok_or_else(|| {
                        io::Error::other(format!("No bounced clip for track '{}'", track.name))
                    })?;
                for format in &export_formats {
                    let stem_file = stem_dir.join(format!(
                        "{}_{}.{}",
                        Self::sanitize_export_component(&track.name),
                        stem_mode_label,
                        Self::export_format_extension(*format)
                    ));
                    fs::rename(&temp_path, &stem_file)?;
                }
                continue;
            }

            let _normalize_params = ExportNormalizeParams {
                mode: normalize_mode,
                target_dbfs: normalize_target_dbfs,
                target_lufs: normalize_target_lufs,
                true_peak_dbtp: normalize_true_peak_dbtp,
                tp_limiter: normalize_tp_limiter,
                sample_rate,
                output_channels,
            };

            let stem_buffer = if is_post_fader {
                let temp_path = track
                    .clips
                    .first()
                    .map(|c| {
                        let p = PathBuf::from(&c.name);
                        if p.is_absolute() {
                            p
                        } else {
                            session_root.join(&c.name)
                        }
                    })
                    .ok_or_else(|| {
                        io::Error::other(format!("No bounced clip for track '{}'", track.name))
                    })?;
                let mut reader = WavReader::open(&temp_path).map_err(|e| {
                    io::Error::other(format!(
                        "Failed to open bounced WAV '{}': {}",
                        temp_path.display(),
                        e
                    ))
                })?;
                let spec = reader.spec();
                let wav_channels = spec.channels.max(1) as usize;
                let samples: Vec<f32> = match spec.sample_format {
                    SampleFormat::Float => reader.samples::<f32>().filter_map(|s| s.ok()).collect(),
                    SampleFormat::Int => match spec.bits_per_sample {
                        16 => reader
                            .samples::<i16>()
                            .filter_map(|s| s.ok())
                            .map(|s| s as f32 * (1.0 / 32768.0))
                            .collect(),
                        24 => reader
                            .samples::<i32>()
                            .filter_map(|s| s.ok())
                            .map(|s| s as f32 * (1.0 / 8_388_608.0))
                            .collect(),
                        32 => reader
                            .samples::<i32>()
                            .filter_map(|s| s.ok())
                            .map(|s| s as f32 * (1.0 / 2_147_483_648.0))
                            .collect(),
                        _ => Vec::new(),
                    },
                };
                let wav_frames = samples.len() / wav_channels;
                let mut buf = vec![0.0_f32; total_length * output_channels];
                let copy_frames = wav_frames.min(total_length);
                if wav_channels == output_channels {
                    let copy_samples = copy_frames * output_channels;
                    buf[..copy_samples].copy_from_slice(&samples[..copy_samples]);
                } else if wav_channels == 1 {
                    for ch in 0..output_channels {
                        let dst_offset = ch;
                        for frame in 0..copy_frames {
                            buf[frame * output_channels + dst_offset] = samples[frame];
                        }
                    }
                } else {
                    for frame in 0..copy_frames {
                        for ch in 0..output_channels {
                            let src_ch = ch.min(wav_channels.saturating_sub(1));
                            buf[frame * output_channels + ch] =
                                samples[frame * wav_channels + src_ch];
                        }
                    }
                }
                buf
            } else {
                Self::mix_track_clips_to_channels(
                    &track.clips,
                    session_root,
                    total_length,
                    output_channels,
                    track.level,
                    track.balance,
                    false,
                )?
            };

            for format in &export_formats {
                let stem_file = stem_dir.join(format!(
                    "{}_{}.{}",
                    Self::sanitize_export_component(&track.name),
                    stem_mode_label,
                    Self::export_format_extension(*format)
                ));
                Self::write_export_audio(ExportWriteRequest {
                    export_path: &stem_file,
                    mixed_buffer: &stem_buffer,
                    sample_rate,
                    output_channels,
                    bit_depth,
                    format: *format,
                    codec,
                    metadata: &metadata,
                })?;
            }
            if realtime_fallback {
                let seconds = (total_length as f64 / sample_rate.max(1) as f64).max(0.0);
                tokio::time::sleep(Duration::from_secs_f64(seconds)).await;
            }
        }
        progress_callback(
            1.0,
            Some(format!(
                "Complete (stems written to {})",
                stem_dir.display()
            )),
        );

        for path in &temp_bounce_files {
            let _ = fs::remove_file(path);
        }
        Ok(())
    }

    fn midi_length_in_samples(path: &Path, sample_rate: f64) -> std::io::Result<usize> {
        let bytes = fs::read(path)?;
        let smf = Smf::parse(&bytes).map_err(|e| {
            io::Error::other(format!("Failed to parse MIDI '{}': {e}", path.display()))
        })?;
        let Timing::Metrical(ppq) = smf.header.timing else {
            return Ok(sample_rate.max(1.0) as usize);
        };
        let ppq = u64::from(ppq.as_int().max(1));
        let mut tempo_changes: Vec<(u64, u32)> = vec![(0, 500_000)];
        let mut max_tick = 0_u64;
        for track in &smf.tracks {
            let mut tick = 0_u64;
            for event in track {
                tick = tick.saturating_add(event.delta.as_int() as u64);
                max_tick = max_tick.max(tick);
                if let TrackEventKind::Meta(MetaMessage::Tempo(us_per_q)) = event.kind {
                    tempo_changes.push((tick, us_per_q.as_int()));
                }
            }
        }
        tempo_changes.sort_by_key(|(tick, _)| *tick);
        let mut normalized_tempos: Vec<(u64, u32)> = Vec::with_capacity(tempo_changes.len());
        for (tick, tempo) in tempo_changes {
            if let Some(last) = normalized_tempos.last_mut()
                && last.0 == tick
            {
                last.1 = tempo;
            } else {
                normalized_tempos.push((tick, tempo));
            }
        }
        let ticks_to_samples = |tick: u64| -> usize {
            let mut total_us: u128 = 0;
            let mut prev_tick = 0_u64;
            let mut current_tempo_us = 500_000_u32;
            for (change_tick, tempo_us) in &normalized_tempos {
                if *change_tick > tick {
                    break;
                }
                let span = change_tick.saturating_sub(prev_tick);
                total_us = total_us.saturating_add(
                    u128::from(span).saturating_mul(u128::from(current_tempo_us)) / u128::from(ppq),
                );
                prev_tick = *change_tick;
                current_tempo_us = *tempo_us;
            }
            let rem = tick.saturating_sub(prev_tick);
            total_us = total_us.saturating_add(
                u128::from(rem).saturating_mul(u128::from(current_tempo_us)) / u128::from(ppq),
            );
            ((total_us as f64 / 1_000_000.0) * sample_rate).round() as usize
        };
        Ok(ticks_to_samples(max_tick).max(1))
    }

    fn midi_ticks_to_samples(smf: &Smf<'_>, sample_rate: f64) -> Option<MidiTickMap> {
        let Timing::Metrical(ppq) = smf.header.timing else {
            return None;
        };
        let ppq = u64::from(ppq.as_int().max(1));
        let mut tempo_changes: Vec<(u64, u32)> = vec![(0, 500_000)];
        let mut max_tick = 0_u64;
        for track in &smf.tracks {
            let mut tick = 0_u64;
            for event in track {
                tick = tick.saturating_add(event.delta.as_int() as u64);
                max_tick = max_tick.max(tick);
                if let TrackEventKind::Meta(MetaMessage::Tempo(us_per_q)) = event.kind {
                    tempo_changes.push((tick, us_per_q.as_int()));
                }
            }
        }
        tempo_changes.sort_by_key(|(tick, _)| *tick);
        let mut normalized_tempos: Vec<(u64, u32)> = Vec::with_capacity(tempo_changes.len());
        for (tick, tempo) in tempo_changes {
            if let Some(last) = normalized_tempos.last_mut()
                && last.0 == tick
            {
                last.1 = tempo;
            } else {
                normalized_tempos.push((tick, tempo));
            }
        }
        let normalized_tempos = Arc::new(normalized_tempos);
        let mapper = {
            let normalized_tempos = normalized_tempos.clone();
            move |tick: u64| -> usize {
                let mut total_us: u128 = 0;
                let mut prev_tick = 0_u64;
                let mut current_tempo_us = 500_000_u32;
                for (change_tick, tempo_us) in normalized_tempos.iter() {
                    if *change_tick > tick {
                        break;
                    }
                    let span = change_tick.saturating_sub(prev_tick);
                    total_us = total_us.saturating_add(
                        u128::from(span).saturating_mul(u128::from(current_tempo_us))
                            / u128::from(ppq),
                    );
                    prev_tick = *change_tick;
                    current_tempo_us = *tempo_us;
                }
                let rem = tick.saturating_sub(prev_tick);
                total_us = total_us.saturating_add(
                    u128::from(rem).saturating_mul(u128::from(current_tempo_us)) / u128::from(ppq),
                );
                ((total_us as f64 / 1_000_000.0) * sample_rate).round() as usize
            }
        };
        Some((Box::new(mapper), ppq, max_tick))
    }

    fn parse_midi_clip_for_piano(
        path: &Path,
        sample_rate: f64,
        clip_start: usize,
    ) -> std::io::Result<PianoParseResult> {
        let bytes = fs::read(path)?;
        let smf = Smf::parse(&bytes).map_err(|e| {
            io::Error::other(format!("Failed to parse MIDI '{}': {e}", path.display()))
        })?;
        let Some((ticks_to_samples, ppq, max_tick)) =
            Self::midi_ticks_to_samples(&smf, sample_rate)
        else {
            return Ok((vec![], vec![], vec![], sample_rate.max(1.0) as usize));
        };

        let mut notes = Vec::<PianoNote>::new();
        let mut controllers = Vec::<PianoControllerPoint>::new();
        let mut sysexes = Vec::<PianoSysExPoint>::new();
        let mut active_notes: HashMap<(u8, u8), Vec<(u64, u8)>> = HashMap::new();

        for track in &smf.tracks {
            let mut tick = 0_u64;
            for event in track {
                tick = tick.saturating_add(event.delta.as_int() as u64);
                match event.kind {
                    TrackEventKind::Midi { channel, message } => {
                        let channel = channel.as_int();
                        match message {
                            midly::MidiMessage::NoteOn { key, vel } => {
                                let pitch = key.as_int();
                                let velocity = vel.as_int();
                                if velocity == 0 {
                                    if let Some(starts) = active_notes.get_mut(&(channel, pitch))
                                        && let Some((start_tick, start_vel)) = starts.pop()
                                    {
                                        let start_sample = ticks_to_samples(start_tick);
                                        let end_sample = ticks_to_samples(tick);
                                        let length_samples =
                                            end_sample.saturating_sub(start_sample).max(1);
                                        notes.push(PianoNote {
                                            start_sample,
                                            length_samples,
                                            pitch,
                                            velocity: start_vel,
                                            channel,
                                        });
                                    }
                                } else {
                                    active_notes
                                        .entry((channel, pitch))
                                        .or_default()
                                        .push((tick, velocity));
                                }
                            }
                            midly::MidiMessage::NoteOff { key, .. } => {
                                let pitch = key.as_int();
                                if let Some(starts) = active_notes.get_mut(&(channel, pitch))
                                    && let Some((start_tick, start_vel)) = starts.pop()
                                {
                                    let start_sample = ticks_to_samples(start_tick);
                                    let end_sample = ticks_to_samples(tick);
                                    let length_samples =
                                        end_sample.saturating_sub(start_sample).max(1);
                                    notes.push(PianoNote {
                                        start_sample,
                                        length_samples,
                                        pitch,
                                        velocity: start_vel,
                                        channel,
                                    });
                                }
                            }
                            midly::MidiMessage::Controller { controller, value } => {
                                controllers.push(PianoControllerPoint {
                                    sample: ticks_to_samples(tick),
                                    controller: controller.as_int(),
                                    value: value.as_int(),
                                    channel,
                                });
                            }
                            _ => {}
                        }
                    }
                    TrackEventKind::SysEx(payload) => {
                        let mut data = Vec::with_capacity(payload.len() + 2);
                        data.push(0xF0);
                        data.extend_from_slice(payload);
                        if data.last().copied() != Some(0xF7) {
                            data.push(0xF7);
                        }
                        sysexes.push(PianoSysExPoint {
                            sample: ticks_to_samples(tick),
                            data,
                        });
                    }
                    TrackEventKind::Escape(payload) => {
                        let mut data = Vec::with_capacity(payload.len() + 1);
                        data.push(0xF7);
                        data.extend_from_slice(payload);
                        sysexes.push(PianoSysExPoint {
                            sample: ticks_to_samples(tick),
                            data,
                        });
                    }
                    _ => {}
                }
            }
        }

        for ((channel, pitch), starts) in active_notes {
            for (start_tick, velocity) in starts {
                let start_sample = ticks_to_samples(start_tick);
                let end_sample = ticks_to_samples(start_tick.saturating_add(ppq / 8));
                let length_samples = end_sample.saturating_sub(start_sample).max(1);
                notes.push(PianoNote {
                    start_sample,
                    length_samples,
                    pitch,
                    velocity,
                    channel,
                });
            }
        }

        notes.sort_by_key(|n| (n.start_sample, n.pitch));
        controllers.sort_by_key(|c| (c.sample, c.controller));
        sysexes.sort_by_key(|s| s.sample);
        let mut clip_len = notes
            .iter()
            .map(|n| n.start_sample.saturating_add(n.length_samples))
            .chain(controllers.iter().map(|c| c.sample))
            .chain(sysexes.iter().map(|s| s.sample))
            .max()
            .unwrap_or_else(|| ticks_to_samples(max_tick))
            .max(1);

        let min_sample = notes
            .iter()
            .map(|n| n.start_sample)
            .chain(controllers.iter().map(|c| c.sample))
            .chain(sysexes.iter().map(|s| s.sample))
            .min()
            .unwrap_or(0);
        if min_sample >= clip_start && clip_start > 0 {
            for note in &mut notes {
                note.start_sample = note.start_sample.saturating_sub(clip_start);
            }
            for ctrl in &mut controllers {
                ctrl.sample = ctrl.sample.saturating_sub(clip_start);
            }
            for sysex in &mut sysexes {
                sysex.sample = sysex.sample.saturating_sub(clip_start);
            }
            clip_len = clip_len.saturating_sub(clip_start).max(1);
        }

        Ok((notes, controllers, sysexes, clip_len))
    }

    fn import_midi_to_session(
        src_path: &Path,
        session_root: &Path,
        sample_rate: f64,
    ) -> std::io::Result<(String, usize)> {
        let stem = src_path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("midi");
        let ext = Self::file_extension_lower(src_path).unwrap_or_else(|| "mid".to_string());
        let rel = Self::unique_import_rel_path(session_root, "midi", stem, &ext)?;
        let dst = session_root.join(&rel);
        fs::create_dir_all(session_root.join("midi"))?;
        fs::copy(src_path, &dst).map_err(|e| {
            io::Error::other(format!(
                "Failed to copy MIDI '{}' to '{}': {e}",
                src_path.display(),
                dst.display()
            ))
        })?;
        let length = Self::midi_length_in_samples(&dst, sample_rate)?;
        Ok((rel, length.max(1)))
    }

    fn plugin_node_to_json(
        node: &maolan_engine::message::PluginGraphNode,
        id_to_index: &std::collections::HashMap<usize, usize>,
    ) -> Option<Value> {
        use maolan_engine::message::PluginGraphNode;
        match node {
            PluginGraphNode::TrackInput => Some(json!({"type":"track_input"})),
            PluginGraphNode::TrackOutput => Some(json!({"type":"track_output"})),
            PluginGraphNode::Lv2PluginInstance(id) => id_to_index
                .get(id)
                .copied()
                .map(|idx| json!({"type":"plugin","plugin_index":idx})),
            PluginGraphNode::Vst3PluginInstance(id) => id_to_index
                .get(id)
                .copied()
                .map(|idx| json!({"type":"vst3_plugin","plugin_index":idx})),
            PluginGraphNode::ClapPluginInstance(id) => id_to_index
                .get(id)
                .copied()
                .map(|idx| json!({"type":"clap_plugin","plugin_index":idx})),
        }
    }

    #[allow(dead_code)]
    fn plugin_node_from_json_with_runtime_nodes(
        v: &Value,
        runtime_nodes: &[maolan_engine::message::PluginGraphNode],
    ) -> Option<maolan_engine::message::PluginGraphNode> {
        use maolan_engine::message::PluginGraphNode;
        if let Some(node_name) = v.as_str() {
            return match node_name {
                "TrackInput" => Some(PluginGraphNode::TrackInput),
                "TrackOutput" => Some(PluginGraphNode::TrackOutput),
                _ => None,
            };
        }

        let t = v["type"].as_str()?;
        match t {
            "track_input" => Some(PluginGraphNode::TrackInput),
            "track_output" => Some(PluginGraphNode::TrackOutput),
            "plugin" => runtime_nodes
                .get(v["plugin_index"].as_u64()? as usize)
                .and_then(|node| {
                    matches!(node, PluginGraphNode::Lv2PluginInstance(_)).then(|| node.clone())
                }),
            "vst3_plugin" => runtime_nodes
                .get(v["plugin_index"].as_u64()? as usize)
                .and_then(|node| {
                    matches!(node, PluginGraphNode::Vst3PluginInstance(_)).then(|| node.clone())
                }),
            "clap_plugin" => runtime_nodes
                .get(v["plugin_index"].as_u64()? as usize)
                .and_then(|node| {
                    matches!(node, PluginGraphNode::ClapPluginInstance(_)).then(|| node.clone())
                }),
            _ => None,
        }
    }

    fn plugin_graph_snapshot_to_json(
        previous_graph: Option<&Value>,
        plugins: &[maolan_engine::message::PluginGraphPlugin],
        connections: &[maolan_engine::message::PluginGraphConnection],
    ) -> Value {
        let id_to_index: std::collections::HashMap<usize, usize> = plugins
            .iter()
            .enumerate()
            .map(|(idx, plugin)| (plugin.instance_id, idx))
            .collect();
        let previous_plugin_states = previous_graph
            .and_then(|graph| graph.get("plugins"))
            .and_then(Value::as_array)
            .map(|plugins| {
                plugins
                    .iter()
                    .filter_map(|plugin| {
                        Some((
                            (
                                plugin.get("format")?.as_str()?.to_string(),
                                plugin.get("uri")?.as_str()?.to_string(),
                            ),
                            plugin.get("state").cloned().unwrap_or(Value::Null),
                        ))
                    })
                    .collect::<std::collections::HashMap<(String, String), Value>>()
            })
            .unwrap_or_default();
        let plugins_json = plugins
            .iter()
            .map(|plugin| {
                let state_json = plugin.state.clone().unwrap_or_else(|| {
                    previous_plugin_states
                        .get(&(plugin.format.clone(), plugin.uri.clone()))
                        .cloned()
                        .unwrap_or(Value::Null)
                });
                json!({
                    "format": plugin.format,
                    "uri": plugin.uri,
                    "state": state_json,
                    "bypassed": plugin.bypassed,
                })
            })
            .collect::<Vec<_>>();
        let connections_json = connections
            .iter()
            .filter_map(|connection| {
                let from_node = Self::plugin_node_to_json(&connection.from_node, &id_to_index)?;
                let to_node = Self::plugin_node_to_json(&connection.to_node, &id_to_index)?;
                Some(json!({
                    "from_node": from_node,
                    "from_port": connection.from_port,
                    "to_node": to_node,
                    "to_port": connection.to_port,
                    "kind": Self::kind_to_json(connection.kind),
                }))
            })
            .collect::<Vec<_>>();
        json!({
            "plugins": plugins_json,
            "connections": connections_json,
        })
    }

    #[cfg(all(test, unix, not(target_os = "macos")))]
    fn plugin_graph_saved_state_from_json<T: serde::de::DeserializeOwned>(
        graph: Option<&Value>,
        plugin_index: usize,
    ) -> Option<T> {
        let state = graph
            .and_then(|graph| graph.get("plugins"))
            .and_then(Value::as_array)
            .and_then(|plugins| plugins.get(plugin_index))
            .and_then(|plugin| plugin.get("state"))
            .cloned()?;
        serde_json::from_value(state).ok()
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    fn plugin_graph_json_with_saved_plugin_state(
        graph: Option<&Value>,
        plugin_index: usize,
        state: Value,
    ) -> Option<Value> {
        let mut graph = graph?.clone();
        let plugins = graph.get_mut("plugins")?.as_array_mut()?;
        let plugin = plugins.get_mut(plugin_index)?;
        let plugin_object = plugin.as_object_mut()?;
        plugin_object.insert("state".to_string(), state);
        Some(graph)
    }
    #[cfg(not(all(unix, not(target_os = "macos"))))]
    fn plugin_graph_json_with_saved_plugin_state(
        _graph: Option<&Value>,
        _plugin_index: usize,
        _state: Value,
    ) -> Option<Value> {
        None
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[allow(dead_code)]
    fn plugin_graph_plugin_from_saved_json(
        instance_id: usize,
        plugin: &Value,
        lv2_plugins: &[maolan_engine::lv2::Lv2PluginInfo],
        vst3_plugins: &[maolan_engine::vst3::Vst3PluginInfo],
        clap_plugins: &[maolan_engine::clap::ClapPluginInfo],
    ) -> Option<maolan_engine::message::PluginGraphPlugin> {
        use maolan_engine::message::{PluginGraphNode, PluginGraphPlugin};

        let uri = plugin.get("uri").and_then(Value::as_str)?.to_string();
        match plugin.get("format").and_then(Value::as_str) {
            Some(format) if format.eq_ignore_ascii_case("LV2") => {
                let info = lv2_plugins.iter().find(|info| info.uri == uri);
                Some(PluginGraphPlugin {
                    node: PluginGraphNode::Lv2PluginInstance(instance_id),
                    instance_id,
                    format: "LV2".to_string(),
                    uri: uri.clone(),
                    plugin_id: uri.clone(),
                    name: info
                        .map(|info| info.name.clone())
                        .unwrap_or_else(|| uri.clone()),
                    main_audio_inputs: info.map(|info| info.audio_inputs).unwrap_or(0),
                    main_audio_outputs: info.map(|info| info.audio_outputs).unwrap_or(0),
                    audio_inputs: info.map(|info| info.audio_inputs).unwrap_or(0),
                    audio_outputs: info.map(|info| info.audio_outputs).unwrap_or(0),
                    midi_inputs: info.map(|info| info.midi_inputs).unwrap_or(0),
                    midi_outputs: info.map(|info| info.midi_outputs).unwrap_or(0),
                    state: plugin.get("state").cloned(),
                    bypassed: plugin
                        .get("bypassed")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false),
                })
            }
            Some(format) if format.eq_ignore_ascii_case("VST3") => {
                let info = vst3_plugins.iter().find(|info| info.path == uri);
                Some(PluginGraphPlugin {
                    node: PluginGraphNode::Vst3PluginInstance(instance_id),
                    instance_id,
                    format: "VST3".to_string(),
                    uri: uri.clone(),
                    plugin_id: info.map(|info| info.id.clone()).unwrap_or_default(),
                    name: info
                        .map(|info| info.name.clone())
                        .unwrap_or_else(|| uri.clone()),
                    main_audio_inputs: info.map(|info| info.audio_inputs).unwrap_or(0),
                    main_audio_outputs: info.map(|info| info.audio_outputs).unwrap_or(0),
                    audio_inputs: info.map(|info| info.audio_inputs).unwrap_or(0),
                    audio_outputs: info.map(|info| info.audio_outputs).unwrap_or(0),
                    midi_inputs: info
                        .map(|info| usize::from(info.has_midi_input))
                        .unwrap_or(0),
                    midi_outputs: info
                        .map(|info| usize::from(info.has_midi_output))
                        .unwrap_or(0),
                    state: None,
                    bypassed: plugin
                        .get("bypassed")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false),
                })
            }
            Some(format) if format.eq_ignore_ascii_case("CLAP") => {
                let info = clap_plugins.iter().find(|info| info.path == uri);
                let caps = info.and_then(|info| info.capabilities.as_ref());
                Some(PluginGraphPlugin {
                    node: PluginGraphNode::ClapPluginInstance(instance_id),
                    instance_id,
                    format: "CLAP".to_string(),
                    uri: uri.clone(),
                    plugin_id: uri
                        .split_once("::")
                        .map(|(_, id)| id.to_string())
                        .unwrap_or_default(),
                    name: info
                        .map(|info| info.name.clone())
                        .unwrap_or_else(|| uri.clone()),
                    main_audio_inputs: caps.map(|caps| caps.audio_inputs).unwrap_or(0),
                    main_audio_outputs: caps.map(|caps| caps.audio_outputs).unwrap_or(0),
                    audio_inputs: caps.map(|caps| caps.audio_inputs).unwrap_or(0),
                    audio_outputs: caps.map(|caps| caps.audio_outputs).unwrap_or(0),
                    midi_inputs: caps.map(|caps| caps.midi_inputs).unwrap_or(0),
                    midi_outputs: caps.map(|caps| caps.midi_outputs).unwrap_or(0),
                    state: plugin.get("state").cloned(),
                    bypassed: plugin
                        .get("bypassed")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false),
                })
            }
            _ => None,
        }
    }

    #[cfg(not(all(unix, not(target_os = "macos"))))]
    #[allow(dead_code)]
    fn plugin_graph_plugin_from_saved_json(
        instance_id: usize,
        plugin: &Value,
        vst3_plugins: &[maolan_engine::vst3::Vst3PluginInfo],
        clap_plugins: &[maolan_engine::clap::ClapPluginInfo],
    ) -> Option<maolan_engine::message::PluginGraphPlugin> {
        use maolan_engine::message::{PluginGraphNode, PluginGraphPlugin};

        let uri = plugin.get("uri").and_then(Value::as_str)?.to_string();
        match plugin.get("format").and_then(Value::as_str) {
            Some(format) if format.eq_ignore_ascii_case("LV2") => None,
            Some(format) if format.eq_ignore_ascii_case("VST3") => {
                let info = vst3_plugins.iter().find(|info| info.path == uri);
                Some(PluginGraphPlugin {
                    node: PluginGraphNode::Vst3PluginInstance(instance_id),
                    instance_id,
                    format: "VST3".to_string(),
                    uri: uri.clone(),
                    plugin_id: info.map(|info| info.id.clone()).unwrap_or_default(),
                    name: info
                        .map(|info| info.name.clone())
                        .unwrap_or_else(|| uri.clone()),
                    main_audio_inputs: info.map(|info| info.audio_inputs).unwrap_or(0),
                    main_audio_outputs: info.map(|info| info.audio_outputs).unwrap_or(0),
                    audio_inputs: info.map(|info| info.audio_inputs).unwrap_or(0),
                    audio_outputs: info.map(|info| info.audio_outputs).unwrap_or(0),
                    midi_inputs: info
                        .map(|info| usize::from(info.has_midi_input))
                        .unwrap_or(0),
                    midi_outputs: info
                        .map(|info| usize::from(info.has_midi_output))
                        .unwrap_or(0),
                    state: None,
                    bypassed: plugin
                        .get("bypassed")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false),
                })
            }
            Some(format) if format.eq_ignore_ascii_case("CLAP") => {
                let info = clap_plugins.iter().find(|info| info.path == uri);
                let caps = info.and_then(|info| info.capabilities.as_ref());
                Some(PluginGraphPlugin {
                    node: PluginGraphNode::ClapPluginInstance(instance_id),
                    instance_id,
                    format: "CLAP".to_string(),
                    uri: uri.clone(),
                    plugin_id: uri
                        .split_once("::")
                        .map(|(_, id)| id.to_string())
                        .unwrap_or_default(),
                    name: info
                        .map(|info| info.name.clone())
                        .unwrap_or_else(|| uri.clone()),
                    main_audio_inputs: caps.map(|caps| caps.audio_inputs).unwrap_or(0),
                    main_audio_outputs: caps.map(|caps| caps.audio_outputs).unwrap_or(0),
                    audio_inputs: caps.map(|caps| caps.audio_inputs).unwrap_or(0),
                    audio_outputs: caps.map(|caps| caps.audio_outputs).unwrap_or(0),
                    midi_inputs: caps.map(|caps| caps.midi_inputs).unwrap_or(0),
                    midi_outputs: caps.map(|caps| caps.midi_outputs).unwrap_or(0),
                    state: plugin.get("state").cloned(),
                    bypassed: plugin
                        .get("bypassed")
                        .and_then(|v| v.as_bool())
                        .unwrap_or(false),
                })
            }
            _ => None,
        }
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[allow(dead_code)]
    fn plugin_graph_snapshot_from_json(
        graph: Option<&Value>,
        lv2_plugins: &[maolan_engine::lv2::Lv2PluginInfo],
        vst3_plugins: &[maolan_engine::vst3::Vst3PluginInfo],
        clap_plugins: &[maolan_engine::clap::ClapPluginInfo],
    ) -> maolan_engine::message::PluginGraphSnapshot {
        let Some(graph) = graph else {
            return (Vec::new(), Vec::new());
        };
        let mut plugins = Vec::new();
        let mut runtime_nodes = Vec::new();
        if let Some(plugin_values) = graph.get("plugins").and_then(Value::as_array) {
            for (idx, plugin) in plugin_values.iter().enumerate() {
                let Some(saved) = Self::plugin_graph_plugin_from_saved_json(
                    idx,
                    plugin,
                    lv2_plugins,
                    vst3_plugins,
                    clap_plugins,
                ) else {
                    continue;
                };
                runtime_nodes.push(saved.node.clone());
                plugins.push(saved);
            }
        }

        let mut connections = Vec::new();
        if let Some(connection_values) = graph.get("connections").and_then(Value::as_array) {
            for connection in connection_values {
                let Some(from_node) = Self::plugin_node_from_json_with_runtime_nodes(
                    &connection["from_node"],
                    &runtime_nodes,
                ) else {
                    continue;
                };
                let Some(to_node) = Self::plugin_node_from_json_with_runtime_nodes(
                    &connection["to_node"],
                    &runtime_nodes,
                ) else {
                    continue;
                };
                let Some(kind) = Self::kind_from_json(&connection["kind"]) else {
                    continue;
                };
                connections.push(maolan_engine::message::PluginGraphConnection {
                    from_node,
                    from_port: connection["from_port"].as_u64().unwrap_or(0) as usize,
                    to_node,
                    to_port: connection["to_port"].as_u64().unwrap_or(0) as usize,
                    kind,
                });
            }
        }

        (plugins, connections)
    }

    #[cfg(not(all(unix, not(target_os = "macos"))))]
    #[allow(dead_code)]
    fn plugin_graph_snapshot_from_json(
        graph: Option<&Value>,
        vst3_plugins: &[maolan_engine::vst3::Vst3PluginInfo],
        clap_plugins: &[maolan_engine::clap::ClapPluginInfo],
    ) -> maolan_engine::message::PluginGraphSnapshot {
        let Some(graph) = graph else {
            return (Vec::new(), Vec::new());
        };
        let mut plugins = Vec::new();
        let mut runtime_nodes = Vec::new();
        if let Some(plugin_values) = graph.get("plugins").and_then(Value::as_array) {
            for (idx, plugin) in plugin_values.iter().enumerate() {
                let Some(saved) = Self::plugin_graph_plugin_from_saved_json(
                    idx,
                    plugin,
                    vst3_plugins,
                    clap_plugins,
                ) else {
                    continue;
                };
                runtime_nodes.push(saved.node.clone());
                plugins.push(saved);
            }
        }

        let mut connections = Vec::new();
        if let Some(connection_values) = graph.get("connections").and_then(Value::as_array) {
            for connection in connection_values {
                let Some(from_node) = Self::plugin_node_from_json_with_runtime_nodes(
                    &connection["from_node"],
                    &runtime_nodes,
                ) else {
                    continue;
                };
                let Some(to_node) = Self::plugin_node_from_json_with_runtime_nodes(
                    &connection["to_node"],
                    &runtime_nodes,
                ) else {
                    continue;
                };
                let Some(kind) = Self::kind_from_json(&connection["kind"]) else {
                    continue;
                };
                connections.push(maolan_engine::message::PluginGraphConnection {
                    from_node,
                    from_port: connection["from_port"].as_u64().unwrap_or(0) as usize,
                    to_node,
                    to_port: connection["to_port"].as_u64().unwrap_or(0) as usize,
                    kind,
                });
            }
        }

        (plugins, connections)
    }

    fn kind_to_json(kind: Kind) -> Value {
        match kind {
            Kind::Audio => json!("audio"),
            Kind::MIDI => json!("midi"),
        }
    }

    #[allow(dead_code)]
    fn kind_from_json(v: &Value) -> Option<Kind> {
        match v.as_str()? {
            "audio" | "Audio" => Some(Kind::Audio),
            "midi" | "MIDI" => Some(Kind::MIDI),
            _ => None,
        }
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    fn lv2_state_to_json(state: &maolan_engine::message::Lv2PluginState) -> Value {
        let port_values = state
            .port_values
            .iter()
            .map(|p| json!({"index": p.index, "value": p.value}))
            .collect::<Vec<_>>();
        let properties = state
            .properties
            .iter()
            .map(|p| {
                json!({
                    "key_uri": p.key_uri,
                    "type_uri": p.type_uri,
                    "flags": p.flags,
                    "value": p.value,
                })
            })
            .collect::<Vec<_>>();
        json!({
            "port_values": port_values,
            "properties": properties,
        })
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    fn lv2_state_from_json(v: &Value) -> Option<maolan_engine::message::Lv2PluginState> {
        let port_values = v["port_values"]
            .as_array()
            .map(|arr| {
                arr.iter()
                    .filter_map(|item| {
                        Some(maolan_engine::message::Lv2StatePortValue {
                            index: item["index"].as_u64()? as u32,
                            value: item["value"].as_f64()? as f32,
                        })
                    })
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        let properties = v["properties"]
            .as_array()
            .map(|arr| {
                arr.iter()
                    .filter_map(|item| {
                        Some(maolan_engine::message::Lv2StateProperty {
                            key_uri: item["key_uri"].as_str()?.to_string(),
                            type_uri: item["type_uri"].as_str()?.to_string(),
                            flags: item["flags"].as_u64().unwrap_or(0) as u32,
                            value: item["value"]
                                .as_array()?
                                .iter()
                                .map(|b| b.as_u64().unwrap_or(0) as u8)
                                .collect(),
                        })
                    })
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        Some(maolan_engine::message::Lv2PluginState {
            port_values,
            properties,
        })
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    fn clap_state_from_json(v: &Value) -> Option<maolan_engine::clap::ClapPluginState> {
        serde_json::from_value(v.clone()).ok()
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    fn track_plugin_list_view(&self) -> iced::Element<'_, Message> {
        let state = self.state.blocking_read();
        let title = Self::plugin_graph_title(&state);

        let mut lv2_items = Vec::new();
        let filter = self.plugin_filter.trim().to_lowercase();
        for plugin in &state.lv2_plugins {
            if !filter.is_empty() {
                let name = plugin.name.to_lowercase();
                let uri = plugin.uri.to_lowercase();
                if !name.contains(&filter) && !uri.contains(&filter) {
                    continue;
                }
            }
            let is_selected = self.selected_lv2_plugins.contains(&plugin.uri);
            let row_content: iced::Element<'_, Message> = row![
                text(if is_selected { "[x]" } else { "[ ]" }),
                text(format!(
                    "{} (a:{}/{}, m:{}/{})",
                    plugin.name,
                    plugin.audio_inputs,
                    plugin.audio_outputs,
                    plugin.midi_inputs,
                    plugin.midi_outputs
                ))
                .width(Length::Fill),
            ]
            .spacing(8)
            .width(Length::Fill)
            .into();

            let row_button = if is_selected {
                button(row_content).style(button::primary)
            } else {
                button(row_content).style(button::text)
            };
            lv2_items.push(
                row_button
                    .width(Length::Fill)
                    .on_press(Message::SelectLv2Plugin(plugin.uri.clone()))
                    .into(),
            );
        }
        let lv2_list = column(lv2_items);

        let mut clap_items = Vec::new();
        let clap_filter = self.clap_plugin_filter.trim().to_lowercase();
        for plugin in &state.clap_plugins {
            if !clap_filter.is_empty() {
                let name = plugin.name.to_lowercase();
                let path = plugin.path.to_lowercase();
                if !name.contains(&clap_filter) && !path.contains(&clap_filter) {
                    continue;
                }
            }
            let is_selected = self.selected_clap_plugins.contains(&plugin.path);

            let mut capability_icons = String::new();
            if let Some(caps) = &plugin.capabilities {
                if caps.has_gui {
                    capability_icons.push_str("\u{1F5BC} ");
                }
                if caps.has_params {
                    capability_icons.push_str("\u{2699} ");
                }
                if caps.has_state {
                    capability_icons.push_str("\u{1F4BE} ");
                }
            }

            let row_content: iced::Element<'_, Message> = row![
                text(if is_selected { "[x]" } else { "[ ]" }),
                text(plugin.name.clone()).width(Length::Fill),
                text(capability_icons),
            ]
            .spacing(8)
            .width(Length::Fill)
            .into();
            let row_button = if is_selected {
                button(row_content).style(button::primary)
            } else {
                button(row_content).style(button::text)
            };
            clap_items.push(
                row_button
                    .width(Length::Fill)
                    .on_press(Message::SelectClapPlugin(plugin.path.clone()))
                    .into(),
            );
        }
        let clap_list = column(clap_items);

        let mut vst3_items = Vec::new();
        let vst3_filter = self.vst3_plugin_filter.trim().to_lowercase();
        for plugin in &state.vst3_plugins {
            if !vst3_filter.is_empty() {
                let name = plugin.name.to_lowercase();
                let path = plugin.path.to_lowercase();
                if !name.contains(&vst3_filter) && !path.contains(&vst3_filter) {
                    continue;
                }
            }
            let is_selected = self.selected_vst3_plugins.contains(&plugin.path);
            let row_content: iced::Element<'_, Message> = row![
                text(if is_selected { "[x]" } else { "[ ]" }),
                text(plugin.name.clone()).width(Length::Fill),
            ]
            .spacing(8)
            .width(Length::Fill)
            .into();
            let row_button = if is_selected {
                button(row_content).style(button::primary)
            } else {
                button(row_content).style(button::text)
            };
            vst3_items.push(
                row_button
                    .width(Length::Fill)
                    .on_press(Message::SelectVst3Plugin(plugin.path.clone()))
                    .into(),
            );
        }
        let vst3_list = column(vst3_items);

        let plugin_controls = match self.plugin_format {
            PluginFormat::Lv2 => {
                let load = if self.selected_lv2_plugins.is_empty() {
                    button("Load")
                } else {
                    button(text(format!("Load ({})", self.selected_lv2_plugins.len())))
                        .on_press(Message::LoadSelectedLv2Plugins)
                };
                column![
                    text_input("Filter LV2 plugins...", &self.plugin_filter)
                        .on_input(Message::FilterLv2Plugins)
                        .width(Length::Fill),
                    scrollable(lv2_list).height(Length::Fill),
                    row![
                        load,
                        pick_list(
                            Self::supported_plugin_formats(),
                            Some(self.plugin_format),
                            Message::PluginFormatSelected,
                        ),
                    ]
                    .spacing(10),
                ]
                .spacing(10)
            }
            PluginFormat::Vst3 => {
                let load = if self.selected_vst3_plugins.is_empty() {
                    button("Load")
                } else {
                    button(text(format!("Load ({})", self.selected_vst3_plugins.len())))
                        .on_press(Message::LoadSelectedVst3Plugins)
                };
                column![
                    text_input("Filter VST3 plugins...", &self.vst3_plugin_filter)
                        .on_input(Message::FilterVst3Plugins)
                        .width(Length::Fill),
                    scrollable(vst3_list).height(Length::Fill),
                    row![
                        load,
                        pick_list(
                            Self::supported_plugin_formats(),
                            Some(self.plugin_format),
                            Message::PluginFormatSelected,
                        ),
                    ]
                    .spacing(10),
                ]
                .spacing(10)
            }
            PluginFormat::Clap => {
                let load = if self.selected_clap_plugins.is_empty() {
                    button("Load")
                } else {
                    button(text(format!("Load ({})", self.selected_clap_plugins.len())))
                        .on_press(Message::LoadSelectedClapPlugins)
                };
                column![
                    text_input("Filter CLAP plugins...", &self.clap_plugin_filter)
                        .on_input(Message::FilterClapPlugin)
                        .width(Length::Fill),
                    scrollable(clap_list).height(Length::Fill),
                    row![
                        load,
                        pick_list(
                            Self::supported_plugin_formats(),
                            Some(self.plugin_format),
                            Message::PluginFormatSelected,
                        ),
                    ]
                    .spacing(10),
                ]
                .spacing(10)
            }
        };

        container(
            column![
                text(title),
                plugin_controls,
                row![
                    button("Close")
                        .on_press(Message::Cancel)
                        .style(button::secondary),
                ]
                .spacing(10),
            ]
            .spacing(10),
        )
        .style(|_theme| crate::style::app_background())
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }

    #[cfg(target_os = "macos")]
    fn track_plugin_list_view(&self) -> iced::Element<'_, Message> {
        let state = self.state.blocking_read();
        let title = Self::plugin_graph_title(&state);
        let mut vst3_items = Vec::new();
        let filter = self.vst3_plugin_filter.trim().to_lowercase();
        for plugin in &state.vst3_plugins {
            if !filter.is_empty() {
                let name = plugin.name.to_lowercase();
                let path = plugin.path.to_lowercase();
                if !name.contains(&filter) && !path.contains(&filter) {
                    continue;
                }
            }
            let is_selected = self.selected_vst3_plugins.contains(&plugin.path);
            let row_content: iced::Element<'_, Message> = row![
                text(if is_selected { "[x]" } else { "[ ]" }),
                text(plugin.name.clone()).width(Length::Fill),
            ]
            .spacing(8)
            .width(Length::Fill)
            .into();
            let row_button = if is_selected {
                button(row_content).style(button::primary)
            } else {
                button(row_content).style(button::text)
            };
            vst3_items.push(
                row_button
                    .width(Length::Fill)
                    .on_press(Message::SelectVst3Plugin(plugin.path.clone()))
                    .into(),
            );
        }
        let vst3_list = column(vst3_items);

        let mut clap_items = Vec::new();
        let clap_filter = self.clap_plugin_filter.trim().to_lowercase();
        for plugin in &state.clap_plugins {
            if !clap_filter.is_empty() {
                let name = plugin.name.to_lowercase();
                let path = plugin.path.to_lowercase();
                if !name.contains(&clap_filter) && !path.contains(&clap_filter) {
                    continue;
                }
            }
            let is_selected = self.selected_clap_plugins.contains(&plugin.path);

            let mut capability_icons = String::new();
            if let Some(caps) = &plugin.capabilities {
                if caps.has_gui {
                    capability_icons.push_str("\u{1F5BC} ");
                }
                if caps.has_params {
                    capability_icons.push_str("\u{2699} ");
                }
                if caps.has_state {
                    capability_icons.push_str("\u{1F4BE} ");
                }
            }

            let row_content: iced::Element<'_, Message> = row![
                text(if is_selected { "[x]" } else { "[ ]" }),
                text(plugin.name.clone()).width(Length::Fill),
                text(capability_icons),
            ]
            .spacing(8)
            .width(Length::Fill)
            .into();
            let row_button = if is_selected {
                button(row_content).style(button::primary)
            } else {
                button(row_content).style(button::text)
            };
            clap_items.push(
                row_button
                    .width(Length::Fill)
                    .on_press(Message::SelectClapPlugin(plugin.path.clone()))
                    .into(),
            );
        }
        let clap_list = column(clap_items);

        let plugin_controls = if self.plugin_format == PluginFormat::Clap {
            let load = if self.selected_clap_plugins.is_empty() {
                button("Load")
            } else {
                button(text(format!("Load ({})", self.selected_clap_plugins.len())))
                    .on_press(Message::LoadSelectedClapPlugins)
            };
            column![
                text_input("Filter CLAP plugins...", &self.clap_plugin_filter)
                    .on_input(Message::FilterClapPlugin)
                    .width(Length::Fill),
                scrollable(clap_list).height(Length::Fill),
                row![
                    load,
                    pick_list(
                        Self::supported_plugin_formats(),
                        Some(self.plugin_format),
                        Message::PluginFormatSelected,
                    ),
                ]
                .spacing(10),
            ]
            .spacing(10)
        } else {
            let load = if self.selected_vst3_plugins.is_empty() {
                button("Load")
            } else {
                button(text(format!("Load ({})", self.selected_vst3_plugins.len())))
                    .on_press(Message::LoadSelectedVst3Plugins)
            };
            column![
                text_input("Filter VST3 plugins...", &self.vst3_plugin_filter)
                    .on_input(Message::FilterVst3Plugins)
                    .width(Length::Fill),
                scrollable(vst3_list).height(Length::Fill),
                row![
                    load,
                    pick_list(
                        Self::supported_plugin_formats(),
                        Some(self.plugin_format),
                        Message::PluginFormatSelected,
                    ),
                ]
                .spacing(10),
            ]
            .spacing(10)
        };

        container(
            column![
                text(title),
                plugin_controls,
                row![
                    button("Close")
                        .on_press(Message::Cancel)
                        .style(button::secondary),
                ]
                .spacing(10),
            ]
            .spacing(10),
        )
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }

    fn send(&self, action: Action) -> Task<Message> {
        Task::perform(
            async move { CLIENT.send(EngineMessage::Request(action)).await },
            |result| match result {
                Ok(_) => Message::SendMessageFinished(Ok(())),
                Err(_) => Message::Response(Err("Channel closed".to_string())),
            },
        )
    }

    fn selected_export_formats(&self) -> Vec<ExportFormat> {
        let mut formats = Vec::new();
        if self.export_format_wav {
            formats.push(ExportFormat::Wav);
        }
        if self.export_format_mp3 {
            formats.push(ExportFormat::Mp3);
        }
        if self.export_format_ogg {
            formats.push(ExportFormat::Ogg);
        }
        if self.export_format_flac {
            formats.push(ExportFormat::Flac);
        }
        formats
    }

    fn export_bit_depth_options(formats: &[ExportFormat]) -> Vec<ExportBitDepth> {
        if formats
            .iter()
            .any(|f| matches!(f, ExportFormat::Wav | ExportFormat::Flac))
        {
            EXPORT_BIT_DEPTH_ALL.to_vec()
        } else {
            vec![ExportBitDepth::Float32]
        }
    }

    fn export_format_extension(format: ExportFormat) -> &'static str {
        match format {
            ExportFormat::Wav => "wav",
            ExportFormat::Mp3 => "mp3",
            ExportFormat::Ogg => "ogg",
            ExportFormat::Flac => "flac",
        }
    }

    fn export_base_path(path: PathBuf) -> PathBuf {
        let known_ext = path
            .extension()
            .and_then(|e| e.to_str())
            .map(|e| e.to_ascii_lowercase())
            .is_some_and(|e| matches!(e.as_str(), "wav" | "mp3" | "ogg" | "flac"));
        if known_ext {
            path.with_extension("")
        } else {
            path
        }
    }

    fn sanitize_export_component(value: &str) -> String {
        let mut out = String::with_capacity(value.len());
        for ch in value.chars() {
            if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
                out.push(ch);
            } else {
                out.push('_');
            }
        }
        if out.is_empty() {
            "track".to_string()
        } else {
            out
        }
    }

    fn export_settings_view(&self) -> iced::Element<'_, Message> {
        let last_message = self.state.blocking_read().message.clone();
        let selected_formats = self.selected_export_formats();
        let bit_depth_options = Self::export_bit_depth_options(&selected_formats);
        let available_hw_out_ports = self.available_export_hw_out_ports();
        let mp3_supported = self.export_mp3_supported_for_current_settings();
        let hw_out_column_count = if available_hw_out_ports.len() > 16 {
            4
        } else if available_hw_out_ports.len() > 8 {
            2
        } else {
            1
        };
        let hw_out_rows_per_column = available_hw_out_ports
            .len()
            .div_ceil(hw_out_column_count.max(1));
        let selected_bit_depth = if bit_depth_options.contains(&self.export_bit_depth) {
            self.export_bit_depth
        } else {
            bit_depth_options
                .first()
                .copied()
                .unwrap_or(ExportBitDepth::Int24)
        };

        let body = container(
            scrollable(
                container(
                    column![
                        text("Export session").size(16),
                        row![
                            text("Formats:"),
                            checkbox(self.export_format_wav)
                                .label("WAV")
                                .on_toggle(Message::ExportFormatWavToggled),
                            if mp3_supported {
                                checkbox(self.export_format_mp3)
                                    .label("MP3")
                                    .on_toggle(Message::ExportFormatMp3Toggled)
                            } else {
                                checkbox(false).label("MP3 (mono/stereo only)")
                            },
                            checkbox(self.export_format_ogg)
                                .label("OGG")
                                .on_toggle(Message::ExportFormatOggToggled),
                            checkbox(self.export_format_flac)
                                .label("FLAC")
                                .on_toggle(Message::ExportFormatFlacToggled),
                        ]
                        .spacing(10)
                        .align_y(iced::Alignment::Center),
                        row![
                            text("Sample rate (Hz):"),
                            pick_list(
                                STANDARD_EXPORT_SAMPLE_RATES.to_vec(),
                                Some(self.export_sample_rate_hz),
                                Message::ExportSampleRateSelected
                            )
                            .placeholder("Choose sample rate")
                            .width(Length::Fixed(220.0)),
                        ]
                        .spacing(10)
                        .align_y(iced::Alignment::Center),
                        if selected_formats
                            .iter()
                            .any(|f| matches!(f, ExportFormat::Wav | ExportFormat::Flac))
                        {
                            row![
                                text("Bit depth:"),
                                pick_list(
                                    bit_depth_options,
                                    Some(selected_bit_depth),
                                    Message::ExportBitDepthSelected
                                )
                                .placeholder("Choose bit depth"),
                            ]
                            .spacing(10)
                            .align_y(iced::Alignment::Center)
                        } else {
                            row![text("Bit depth: codec-managed (lossy formats)")]
                                .spacing(10)
                                .align_y(iced::Alignment::Center)
                        },
                        if self.export_format_mp3 {
                            row![
                                text("MP3 mode:"),
                                pick_list(
                                    EXPORT_MP3_MODE_ALL.to_vec(),
                                    Some(self.export_mp3_mode),
                                    Message::ExportMp3ModeSelected
                                )
                                .placeholder("Choose MP3 mode")
                                .width(Length::Fixed(160.0)),
                                text("Bitrate (kbps):"),
                                pick_list(
                                    vec![96_u16, 128, 160, 192, 224, 256, 320],
                                    Some(self.export_mp3_bitrate_kbps),
                                    Message::ExportMp3BitrateSelected
                                )
                                .placeholder("Bitrate")
                                .width(Length::Fixed(140.0)),
                            ]
                            .spacing(10)
                            .align_y(iced::Alignment::Center)
                        } else {
                            row![text("")].spacing(10).align_y(iced::Alignment::Center)
                        },
                        if self.export_format_ogg {
                            row![
                                text("OGG quality (-0.1..1.0):"),
                                text_input("0.6", &self.export_ogg_quality_input)
                                    .on_input(Message::ExportOggQualityInput)
                                    .width(Length::Fixed(140.0)),
                            ]
                            .spacing(10)
                            .align_y(iced::Alignment::Center)
                        } else {
                            row![text("")].spacing(10).align_y(iced::Alignment::Center)
                        },
                        row![
                            text("Render mode:"),
                            pick_list(
                                EXPORT_RENDER_MODE_ALL.to_vec(),
                                Some(self.export_render_mode),
                                Message::ExportRenderModeSelected
                            )
                            .placeholder("Choose render mode")
                            .width(Length::Fixed(220.0)),
                        ]
                        .spacing(10)
                        .align_y(iced::Alignment::Center),
                        if matches!(self.export_render_mode, ExportRenderMode::Mixdown) {
                            container(
                                column![
                                    text("Export hw:out ports:"),
                                    if available_hw_out_ports.is_empty() {
                                        iced::Element::from(text(
                                            "No hardware output ports are available.",
                                        ))
                                    } else {
                                        iced::Element::from(row(
                                            available_hw_out_ports
                                                .chunks(hw_out_rows_per_column.max(1))
                                                .map(|ports| {
                                                    column(
                                                        ports
                                                            .iter()
                                                            .map(|port| {
                                                                checkbox(
                                                                    self.export_hw_out_ports
                                                                        .contains(port),
                                                                )
                                                                .label(format!(
                                                                    "hw:out {}",
                                                                    port + 1
                                                                ))
                                                                .on_toggle({
                                                                    let port = *port;
                                                                    move |enabled| {
                                                                        Message::ExportHwOutPortToggled(
                                                                            port, enabled,
                                                                        )
                                                                    }
                                                                })
                                                                .into()
                                                            })
                                                            .collect::<Vec<
                                                                iced::Element<'_, Message>,
                                                            >>(),
                                                    )
                                                    .spacing(6)
                                                    .width(Length::FillPortion(1))
                                                    .into()
                                                })
                                                .collect::<Vec<iced::Element<'_, Message>>>(),
                                        )
                                        .spacing(24)
                                        .width(Length::Fill))
                                    }
                                ]
                                .spacing(8),
                            )
                        } else {
                            container(text(
                                "Stem export writes one channel per track output port.",
                            ))
                        },
                        checkbox(self.export_realtime_fallback)
                            .label("Real-time fallback render")
                            .on_toggle(Message::ExportRealtimeFallbackToggled),
                        row![
                            checkbox(self.export_master_limiter)
                                .label("Master limiter")
                                .on_toggle(Message::ExportMasterLimiterToggled),
                            text("Ceiling (dBTP):"),
                            text_input("-1.0", &self.export_master_limiter_ceiling_input)
                                .on_input(Message::ExportMasterLimiterCeilingInput)
                                .width(Length::Fixed(110.0)),
                        ]
                        .spacing(10)
                        .align_y(iced::Alignment::Center),
                        checkbox(self.export_normalize)
                            .label("Normalize")
                            .on_toggle(Message::ExportNormalizeToggled),
                        if self.export_normalize {
                            container(
                                column![
                                    row![
                                        text("Mode:"),
                                        pick_list(
                                            EXPORT_NORMALIZE_MODE_ALL.to_vec(),
                                            Some(self.export_normalize_mode),
                                            Message::ExportNormalizeModeSelected
                                        )
                                        .placeholder("Choose mode")
                                        .width(Length::Fixed(180.0)),
                                    ]
                                    .spacing(10)
                                    .align_y(iced::Alignment::Center),
                                    if matches!(
                                        self.export_normalize_mode,
                                        ExportNormalizeMode::Peak
                                    ) {
                                        row![
                                            text("Target (dBFS):"),
                                            text_input("0.0", &self.export_normalize_dbfs_input)
                                                .on_input(Message::ExportNormalizeDbfsInput)
                                                .width(Length::Fixed(120.0)),
                                        ]
                                        .spacing(10)
                                        .align_y(iced::Alignment::Center)
                                    } else {
                                        row![
                                            text("Target (LUFS):"),
                                            text_input("-23.0", &self.export_normalize_lufs_input)
                                                .on_input(Message::ExportNormalizeLufsInput)
                                                .width(Length::Fixed(120.0)),
                                            text("TP ceiling (dBTP):"),
                                            text_input("-1.0", &self.export_normalize_dbtp_input)
                                                .on_input(Message::ExportNormalizeDbtpInput)
                                                .width(Length::Fixed(120.0)),
                                        ]
                                        .spacing(10)
                                        .align_y(iced::Alignment::Center)
                                    },
                                    if matches!(
                                        self.export_normalize_mode,
                                        ExportNormalizeMode::Loudness
                                    ) {
                                        checkbox(self.export_normalize_tp_limiter)
                                            .label("Use true-peak limiter")
                                            .on_toggle(Message::ExportNormalizeLimiterToggled)
                                    } else {
                                        checkbox(false).label("")
                                    }
                                ]
                                .spacing(8),
                            )
                        } else {
                            container(row![text("")])
                        },
                        row![
                            button("Export").on_press(Message::ExportSettingsConfirm),
                            button("Cancel")
                                .on_press(Message::Cancel)
                                .style(button::secondary),
                        ]
                        .spacing(10),
                        text("Use standard sample rates for broad compatibility."),
                    ]
                    .align_x(iced::Alignment::Start)
                    .spacing(12),
                )
                .width(Length::Fill),
            )
            .width(Length::Fill)
            .height(Length::Fill),
        )
        .style(|_theme| crate::style::app_background())
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .align_x(iced::Alignment::Center)
        .align_y(iced::Alignment::Center);

        column![
            body,
            container(text(format!("Last message: {}", last_message)))
                .width(Length::Fill)
                .padding([0, 20]),
        ]
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }

    fn preferences_auto_device_option() -> PreferencesDeviceOption {
        PreferencesDeviceOption {
            id: PREF_DEVICE_AUTO_ID.to_string(),
            label: "Auto".to_string(),
        }
    }

    fn preferences_selected_device_option(
        options: &[PreferencesDeviceOption],
        selected_id: Option<&str>,
    ) -> Option<PreferencesDeviceOption> {
        let selected_id = selected_id.unwrap_or(PREF_DEVICE_AUTO_ID);
        options.iter().find(|opt| opt.id == selected_id).cloned()
    }

    fn preferences_output_device_options(&self) -> Vec<PreferencesDeviceOption> {
        let mut options = vec![Self::preferences_auto_device_option()];
        let state = self.state.blocking_read();
        #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
        {
            options.extend(state.available_hw.iter().map(|hw| PreferencesDeviceOption {
                id: hw.id.clone(),
                label: hw.label.clone(),
            }));
        }
        #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd")))]
        {
            options.extend(state.available_hw.iter().map(|hw| PreferencesDeviceOption {
                id: hw.clone(),
                label: hw.clone(),
            }));
        }
        options
    }

    fn preferences_input_device_options(&self) -> Vec<PreferencesDeviceOption> {
        let mut options = vec![Self::preferences_auto_device_option()];
        let state = self.state.blocking_read();
        #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
        {
            options.extend(state.available_hw.iter().map(|hw| PreferencesDeviceOption {
                id: hw.id.clone(),
                label: hw.label.clone(),
            }));
        }
        #[cfg(target_os = "linux")]
        {
            options.extend(
                state
                    .available_input_hw
                    .iter()
                    .map(|hw| PreferencesDeviceOption {
                        id: hw.id.clone(),
                        label: hw.label.clone(),
                    }),
            );
        }
        #[cfg(target_os = "windows")]
        {
            options.extend(
                state
                    .available_input_hw
                    .iter()
                    .map(|hw| PreferencesDeviceOption {
                        id: hw.clone(),
                        label: hw.clone(),
                    }),
            );
        }
        options
    }

    fn apply_preferred_devices_to_state(state: &mut StateData, prefs: &AppPreferences) {
        #[cfg(any(unix, target_os = "windows"))]
        {
            let bits = if AUDIO_BIT_DEPTH_OPTIONS.contains(&prefs.default_audio_bit_depth) {
                prefs.default_audio_bit_depth
            } else {
                audio_defaults::BIT_DEPTH
            };
            state.oss_bits = bits;
        }
        if let Some(device_id) = prefs.default_output_device_id.as_deref() {
            #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
            if let Some(selected) = state
                .available_hw
                .iter()
                .find(|hw| hw.id == device_id)
                .cloned()
            {
                state.selected_hw = Some(selected);
            }
            #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd")))]
            if state.available_hw.iter().any(|hw| hw == device_id) {
                state.selected_hw = Some(device_id.to_string());
            }
        }
        if let Some(device_id) = prefs.default_input_device_id.as_deref() {
            #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
            if let Some(selected) = state
                .available_hw
                .iter()
                .find(|hw| hw.id == device_id)
                .cloned()
            {
                state.selected_input_hw = Some(selected);
            }
            #[cfg(target_os = "linux")]
            if let Some(selected) = state
                .available_input_hw
                .iter()
                .find(|hw| hw.id == device_id)
                .cloned()
            {
                state.selected_input_hw = Some(selected);
            }
            #[cfg(target_os = "windows")]
            if state.available_input_hw.iter().any(|hw| hw == device_id) {
                state.selected_input_hw = Some(device_id.to_string());
            }
        }
        #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
        if let Some(selected) = state.selected_hw.as_ref()
            && let Some(bits) = selected.preferred_bits()
        {
            state.oss_bits = bits;
        }
        #[cfg(target_os = "windows")]
        if let Some(selected) = state.selected_hw.as_ref() {
            state.oss_bits = crate::state::discover_windows_output_bit_depths(selected)
                .first()
                .copied()
                .unwrap_or(state.oss_bits);
        }
    }

    fn preferences_view(&self) -> iced::Element<'_, Message> {
        let output_options = self.preferences_output_device_options();
        let selected_output = Self::preferences_selected_device_option(
            &output_options,
            self.prefs_default_output_device_id.as_deref(),
        );
        let input_options = self.preferences_input_device_options();
        let selected_input = Self::preferences_selected_device_option(
            &input_options,
            self.prefs_default_input_device_id.as_deref(),
        );
        let mut preferences_column = Column::new().align_x(iced::Alignment::Start).spacing(12);
        preferences_column = preferences_column.push(text("Preferences").size(16));
        preferences_column = preferences_column.push(
            row![
                checkbox(self.prefs_osc_enabled)
                    .label("Enable OSC")
                    .on_toggle(Message::PreferencesOscEnabledToggled),
            ]
            .spacing(10)
            .align_y(iced::Alignment::Center),
        );
        preferences_column = preferences_column.push(
            row![
                text("Default export sample rate (Hz):"),
                pick_list(
                    STANDARD_EXPORT_SAMPLE_RATES.to_vec(),
                    Some(self.prefs_export_sample_rate_hz),
                    Message::PreferencesSampleRateSelected
                )
                .placeholder("Choose sample rate")
                .width(Length::Fixed(220.0)),
            ]
            .spacing(10)
            .align_y(iced::Alignment::Center),
        );
        preferences_column = preferences_column.push(
            row![
                text("Default clip snap mode:"),
                pick_list(
                    SNAP_MODE_ALL.to_vec(),
                    Some(self.prefs_snap_mode),
                    Message::PreferencesSnapModeSelected
                )
                .placeholder("Choose snap mode")
                .width(Length::Fixed(220.0)),
            ]
            .spacing(10)
            .align_y(iced::Alignment::Center),
        );
        preferences_column = preferences_column.push(
            row![
                text("Default MIDI snap mode:"),
                pick_list(
                    SNAP_MODE_ALL.to_vec(),
                    Some(self.prefs_midi_snap_mode),
                    Message::PreferencesMidiSnapModeSelected
                )
                .placeholder("Choose snap mode")
                .width(Length::Fixed(220.0)),
            ]
            .spacing(10)
            .align_y(iced::Alignment::Center),
        );
        #[cfg(unix)]
        {
            preferences_column = preferences_column.push(
                row![
                    text("Default bit depth:"),
                    pick_list(
                        AUDIO_BIT_DEPTH_OPTIONS.to_vec(),
                        Some(self.prefs_audio_bit_depth),
                        Message::PreferencesBitDepthSelected
                    )
                    .placeholder("Choose bit depth")
                    .width(Length::Fixed(220.0)),
                ]
                .spacing(10)
                .align_y(iced::Alignment::Center),
            );
        }
        preferences_column = preferences_column.push(
            row![
                text("Default output device:"),
                pick_list(
                    output_options,
                    selected_output,
                    Message::PreferencesOutputDeviceSelected
                )
                .placeholder("Choose output device")
                .width(Length::Fixed(320.0)),
            ]
            .spacing(10)
            .align_y(iced::Alignment::Center),
        );
        if platform_caps::HAS_SEPARATE_AUDIO_INPUT_DEVICE {
            preferences_column = preferences_column.push(
                row![
                    text("Default input device:"),
                    pick_list(
                        input_options,
                        selected_input,
                        Message::PreferencesInputDeviceSelected
                    )
                    .placeholder("Choose input device")
                    .width(Length::Fixed(320.0)),
                ]
                .spacing(10)
                .align_y(iced::Alignment::Center),
            );
        } else {
            preferences_column = preferences_column
                .push(row![text("")].spacing(10).align_y(iced::Alignment::Center));
        }
        preferences_column = preferences_column.push(
            row![
                button("Save").on_press(Message::PreferencesSave),
                button("Cancel")
                    .on_press(Message::Cancel)
                    .style(button::secondary),
            ]
            .spacing(10),
        );
        container(preferences_column)
            .style(|_theme| crate::style::app_background())
            .padding(20)
            .width(Length::Fill)
            .height(Length::Fill)
            .align_x(iced::Alignment::Center)
            .align_y(iced::Alignment::Center)
            .into()
    }

    fn generate_audio_view(&self) -> iced::Element<'_, Message> {
        let session_ready = self.session_dir.is_some();
        let progress_label = if self.generate_audio_in_progress {
            if let Some(operation) = self.generate_audio_operation.as_deref() {
                format!(
                    "{} ({:.0}%)",
                    operation,
                    (self.generate_audio_progress * 100.0).clamp(0.0, 100.0)
                )
            } else {
                format!(
                    "Generating ({:.0}%)",
                    (self.generate_audio_progress * 100.0).clamp(0.0, 100.0)
                )
            }
        } else {
            "Open or save a session before generating audio.".to_string()
        };
        let prompt_label = "Lyrics";
        let generate_button = if self.generate_audio_in_progress || !session_ready {
            button("Generate")
        } else {
            button("Generate").on_press(Message::GenerateAudioSubmit)
        };
        let cancel_button = if self.generate_audio_in_progress {
            button("Cancel")
                .on_press(Message::GenerateAudioCancel)
                .style(button::danger)
        } else {
            button("Close")
                .on_press(Message::Cancel)
                .style(button::secondary)
        };

        container(
            scrollable(
                column![
                    text("Generate Audio").size(16),
                    row![
                        text("Model:"),
                        pick_list(
                            GenerateAudioModelOption::ALL.to_vec(),
                            Some(self.generate_audio_model),
                            Message::GenerateAudioModelSelected
                        )
                        .placeholder("Choose model")
                        .width(Length::Fill),
                    ]
                    .spacing(10)
                    .align_y(iced::Alignment::Center),
                    text_editor(&self.generate_audio_prompt_editor)
                        .on_action(Message::GenerateAudioPromptAction)
                        .height(Length::Fixed(120.0))
                        .placeholder(prompt_label),
                    text_input("Tags (optional)", &self.generate_audio_tags_input)
                        .on_input(Message::GenerateAudioTagsInput)
                        .width(Length::Fill),
                    row![
                        text("Backend:"),
                        pick_list(
                            BurnBackendOption::ALL.to_vec(),
                            Some(self.generate_audio_backend),
                            Message::GenerateAudioBackendSelected
                        )
                        .placeholder("Choose backend")
                        .width(Length::Fill),
                    ]
                    .spacing(10)
                    .align_y(iced::Alignment::Center),
                    row![
                        text("CFG scale"),
                        number_input_f32(
                            &self.generate_audio_cfg_scale_input,
                            0.0..=20.0,
                            0.1,
                            Message::GenerateAudioCfgScaleInput
                        ),
                        text("Steps:"),
                        number_input(
                            &self.generate_audio_steps_input,
                            1..=50,
                            Message::GenerateAudioStepsInput
                        ),
                    ]
                    .spacing(10)
                    .align_y(iced::Alignment::Center),
                    row![
                        text("Seconds total:"),
                        number_input(
                            &self.generate_audio_seconds_total_input,
                            1..=600,
                            Message::GenerateAudioSecondsTotalInput
                        ),
                    ]
                    .spacing(10)
                    .align_y(iced::Alignment::Center),
                    text(progress_label),
                    if self.generate_audio_in_progress {
                        container(progress_bar(
                            0.0..=1.0,
                            self.generate_audio_progress.clamp(0.0, 1.0),
                        ))
                        .width(Length::Fill)
                    } else {
                        container(progress_bar(0.0..=1.0, 0.0)).width(Length::Fill)
                    },
                    row![generate_button, cancel_button].spacing(10),
                ]
                .align_x(iced::Alignment::Start)
                .spacing(12),
            )
            .width(Length::Fill)
            .height(Length::Fill),
        )
        .style(|_theme| crate::style::app_background())
        .padding(12)
        .width(Length::Fixed(360.0))
        .height(Length::Fill)
        .into()
    }

    fn session_metadata_view(&self) -> iced::Element<'_, Message> {
        let state = self.state.blocking_read();
        container(
            column![
                text("Session Metadata").size(16),
                row![
                    text("Author:"),
                    text_input("Author", &state.session_author)
                        .on_input(Message::SessionMetadataAuthorInput)
                        .width(Length::Fixed(260.0)),
                ]
                .spacing(10)
                .align_y(iced::Alignment::Center),
                row![
                    text("Album:"),
                    text_input("Album", &state.session_album)
                        .on_input(Message::SessionMetadataAlbumInput)
                        .width(Length::Fixed(260.0)),
                ]
                .spacing(10)
                .align_y(iced::Alignment::Center),
                row![
                    text("Year:"),
                    text_input("Year", &state.session_year)
                        .on_input(Message::SessionMetadataYearInput)
                        .width(Length::Fixed(120.0)),
                    text("Track #:"),
                    text_input("Track number", &state.session_track_number)
                        .on_input(Message::SessionMetadataTrackNumberInput)
                        .width(Length::Fixed(120.0)),
                ]
                .spacing(10)
                .align_y(iced::Alignment::Center),
                row![
                    text("Genre:"),
                    text_input("Genre", &state.session_genre)
                        .on_input(Message::SessionMetadataGenreInput)
                        .width(Length::Fixed(260.0)),
                ]
                .spacing(10)
                .align_y(iced::Alignment::Center),
                row![
                    button("Save").on_press(Message::SessionMetadataSave),
                    button("Cancel")
                        .on_press(Message::Cancel)
                        .style(button::secondary),
                ]
                .spacing(10),
            ]
            .align_x(iced::Alignment::Start)
            .spacing(12),
        )
        .style(|_theme| crate::style::app_background())
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .align_x(iced::Alignment::Center)
        .align_y(iced::Alignment::Center)
        .into()
    }

    fn autosave_recovery_view(&self) -> iced::Element<'_, Message> {
        let session_label = self
            .pending_recovery_session_dir
            .as_ref()
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_else(|| "<unknown session>".to_string());
        container(
            column![
                text("Autosave Recovery").size(16),
                text(format!(
                    "A newer autosave snapshot was found for:\n{}",
                    session_label
                )),
                row![
                    button("Recover Latest").on_press(Message::RecoverAutosaveSnapshot),
                    button("Ignore").on_press(Message::RecoverAutosaveIgnore),
                ]
                .spacing(10),
            ]
            .align_x(iced::Alignment::Start)
            .spacing(12),
        )
        .style(|_theme| crate::style::app_background())
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .align_x(iced::Alignment::Center)
        .align_y(iced::Alignment::Center)
        .into()
    }

    fn about_view(&self) -> iced::Element<'_, Message> {
        container(
            column![
                text("Maolan DAW").size(20),
                text(format!("Version {}", env!("CARGO_PKG_VERSION"))).size(12),
                text("License: BSD 2-Clause License").size(12),
                button(text("https://maolan.rs").color(Color::from_rgb(0.36, 0.66, 0.98)))
                    .on_press(Message::OpenUrl("https://maolan.rs".to_string()))
                    .style(button::text),
                button("Close")
                    .on_press(Message::Cancel)
                    .style(button::secondary),
            ]
            .align_x(iced::Alignment::Start)
            .spacing(12),
        )
        .style(|_theme| crate::style::app_background())
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .align_x(iced::Alignment::Center)
        .align_y(iced::Alignment::Center)
        .into()
    }

    fn track_color_view(&self, track_name: String) -> iced::Element<'_, Message> {
        let state = self.state.blocking_read();
        let track = state.tracks.iter().find(|t| t.name == track_name);
        let current_color = track
            .and_then(|t| t.color)
            .unwrap_or(Color::from_rgb(0.5, 0.5, 0.5));
        drop(state);
        let color_track_name = track_name.clone();
        container(
            column![
                text(format!("Color for {}", track_name)).size(16),
                color_picker_with_change(
                    true,
                    current_color,
                    container("").width(Length::Fill).height(Length::Fill),
                    Message::TrackColorClear(track_name),
                    move |_color| Message::Cancel,
                    move |color| Message::TrackColorChanged {
                        track_name: color_track_name.clone(),
                        color: Some(color),
                    },
                ),
            ]
            .align_x(iced::Alignment::Start)
            .spacing(12),
        )
        .style(|_theme| crate::style::app_background())
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .align_x(iced::Alignment::Center)
        .align_y(iced::Alignment::Center)
        .into()
    }

    fn unsaved_changes_view(&self) -> iced::Element<'_, Message> {
        let session_label = self
            .session_dir
            .as_ref()
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_else(|| "Untitled session".to_string());
        container(
            column![
                text("Unsaved Changes").size(16),
                text(format!(
                    "Session has unsaved changes:\n{}\n\nSave before closing, discard changes, or cancel.",
                    session_label
                )),
                row![
                    button("Save").on_press(Message::ConfirmCloseSave),
                    button("Discard").on_press(Message::ConfirmCloseDiscard),
                    button("Cancel").on_press(Message::ConfirmCloseCancel),
                ]
                .spacing(10),
            ]
            .align_x(iced::Alignment::Start)
            .spacing(12),
        )
        .style(|_theme| crate::style::app_background())
        .padding(20)
        .width(Length::Fill)
        .height(Length::Fill)
        .align_x(iced::Alignment::Center)
        .align_y(iced::Alignment::Center)
        .into()
    }

    fn update_children(&mut self, message: &Message) {
        self.menu.update(message);
        self.toolbar.update(message);
        self.workspace.update(message);
        self.connections.update(message);
        #[cfg(all(unix, not(target_os = "macos")))]
        self.track_plugins.update(message);
        self.add_track.update(message);
        self.clip_rename.update(message);
        self.track_rename.update(message);
        self.track_group.update(message);
        self.track_marker.update(message);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(unix)]
    use maolan_engine::message::PluginGraphNode;
    #[cfg(unix)]
    use std::path::PathBuf;
    use std::sync::{LazyLock, Mutex};
    use std::time::{SystemTime, UNIX_EPOCH};

    static AUDIO_PEAK_TEST_GUARD: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

    #[test]
    fn normalize_recent_session_paths_trims_deduplicates_and_limits() {
        let paths = vec![
            "  /a  ".to_string(),
            "".to_string(),
            "/b".to_string(),
            "/a".to_string(),
            "   ".to_string(),
        ];

        let normalized = Maolan::normalize_recent_session_paths(paths);
        assert_eq!(normalized, vec!["/a".to_string(), "/b".to_string()]);
        assert!(normalized.len() <= crate::consts::gui_mod::MAX_RECENT_SESSIONS);
    }

    #[test]
    #[cfg(unix)]
    fn kind_json_roundtrip_rejects_unknown_values() {
        assert_eq!(
            Maolan::kind_from_json(&Maolan::kind_to_json(Kind::Audio)),
            Some(Kind::Audio)
        );
        assert_eq!(
            Maolan::kind_from_json(&Maolan::kind_to_json(Kind::MIDI)),
            Some(Kind::MIDI)
        );
        assert_eq!(
            Maolan::kind_from_json(&Value::String("other".to_string())),
            None
        );
        assert_eq!(
            Maolan::kind_from_json(&Value::String("Audio".to_string())),
            Some(Kind::Audio)
        );
        assert_eq!(
            Maolan::kind_from_json(&Value::String("MIDI".to_string())),
            Some(Kind::MIDI)
        );
    }

    #[test]
    #[cfg(all(unix, not(target_os = "macos")))]
    fn plugin_node_from_json_with_runtime_nodes_maps_only_matching_runtime_formats() {
        let runtime_nodes = vec![
            PluginGraphNode::Lv2PluginInstance(10),
            PluginGraphNode::ClapPluginInstance(11),
        ];

        assert_eq!(
            Maolan::plugin_node_from_json_with_runtime_nodes(
                &json!({"type": "plugin", "plugin_index": 0}),
                &runtime_nodes
            ),
            Some(PluginGraphNode::Lv2PluginInstance(10))
        );
        assert_eq!(
            Maolan::plugin_node_from_json_with_runtime_nodes(
                &json!({"type": "clap_plugin", "plugin_index": 1}),
                &runtime_nodes
            ),
            Some(PluginGraphNode::ClapPluginInstance(11))
        );
        assert_eq!(
            Maolan::plugin_node_from_json_with_runtime_nodes(
                &json!({"type": "plugin", "plugin_index": 1}),
                &runtime_nodes
            ),
            None
        );
        assert_eq!(
            Maolan::plugin_node_from_json_with_runtime_nodes(
                &json!({"type": "clap_plugin", "plugin_index": 0}),
                &runtime_nodes
            ),
            None
        );
        assert_eq!(
            Maolan::plugin_node_from_json_with_runtime_nodes(&json!("TrackInput"), &runtime_nodes),
            Some(PluginGraphNode::TrackInput)
        );
        assert_eq!(
            Maolan::plugin_node_from_json_with_runtime_nodes(&json!("TrackOutput"), &runtime_nodes),
            Some(PluginGraphNode::TrackOutput)
        );
    }

    #[test]
    #[cfg(unix)]
    fn plugin_node_to_json_serializes_track_and_plugin_nodes() {
        let mut id_to_index = std::collections::HashMap::new();
        id_to_index.insert(7usize, 2usize);

        assert_eq!(
            Maolan::plugin_node_to_json(&PluginGraphNode::TrackInput, &id_to_index),
            Some(json!({"type":"track_input"}))
        );
        assert_eq!(
            Maolan::plugin_node_to_json(&PluginGraphNode::TrackOutput, &id_to_index),
            Some(json!({"type":"track_output"}))
        );
        #[cfg(all(unix, not(target_os = "macos")))]
        assert_eq!(
            Maolan::plugin_node_to_json(&PluginGraphNode::Lv2PluginInstance(7), &id_to_index),
            Some(json!({"type":"plugin","plugin_index":2}))
        );
        assert_eq!(
            Maolan::plugin_node_to_json(&PluginGraphNode::Vst3PluginInstance(7), &id_to_index),
            Some(json!({"type":"vst3_plugin","plugin_index":2}))
        );
        assert_eq!(
            Maolan::plugin_node_to_json(&PluginGraphNode::ClapPluginInstance(7), &id_to_index),
            Some(json!({"type":"clap_plugin","plugin_index":2}))
        );
    }

    #[test]
    fn zoom_slider_roundtrip_preserves_visible_bars() {
        for visible_bars in [0.25, 1.0, 2.0, 4.0, 16.0, 64.0, 256.0] {
            let slider = visible_bars_to_zoom_slider(visible_bars);
            let roundtrip = zoom_slider_to_visible_bars(slider);
            assert!((roundtrip - visible_bars).abs() < 0.001);
        }
    }

    #[test]
    fn zoom_slider_midpoint_is_geometric_midpoint() {
        let midpoint = zoom_slider_to_visible_bars(0.5);
        assert!((midpoint - 8.0).abs() < 0.001);
    }

    #[test]
    fn session_save_and_load_roundtrip_preserves_arrangement_zoom() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let session_root = std::env::temp_dir().join(format!("maolan_zoom_session_{unique}"));

        let app = Maolan {
            zoom_visible_bars: 6.5,
            ..Maolan::default()
        };
        app.save(session_root.to_string_lossy().to_string())
            .expect("save session");

        let mut restored = Maolan {
            zoom_visible_bars: 42.0,
            ..Maolan::default()
        };
        let _ = restored
            .load(session_root.to_string_lossy().to_string())
            .expect("load session");

        assert!((restored.zoom_visible_bars - 6.5).abs() < f32::EPSILON);

        fs::remove_dir_all(&session_root).expect("cleanup temp session");
    }

    #[test]
    fn session_load_without_saved_arrangement_zoom_keeps_current_zoom() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let session_root =
            std::env::temp_dir().join(format!("maolan_zoom_session_compat_{unique}"));

        let app = Maolan {
            zoom_visible_bars: 5.0,
            ..Maolan::default()
        };
        app.save(session_root.to_string_lossy().to_string())
            .expect("save session");

        let session_path = session_root.join("session.json");
        let mut session: Value =
            serde_json::from_reader(File::open(&session_path).expect("open saved session"))
                .expect("parse saved session");
        session["ui"]
            .as_object_mut()
            .expect("ui object")
            .remove("zoom_visible_bars");
        serde_json::to_writer_pretty(
            File::create(&session_path).expect("rewrite session"),
            &session,
        )
        .expect("write compatibility session");

        let mut restored = Maolan {
            zoom_visible_bars: 42.0,
            ..Maolan::default()
        };
        let _ = restored
            .load(session_root.to_string_lossy().to_string())
            .expect("load session");

        assert!((restored.zoom_visible_bars - 42.0).abs() < f32::EPSILON);

        fs::remove_dir_all(&session_root).expect("cleanup temp session");
    }

    #[test]
    fn session_save_includes_per_track_midi_editor_view_mode() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let session_root =
            std::env::temp_dir().join(format!("maolan_midi_view_mode_save_{unique}"));

        let app = Maolan {
            ..Maolan::default()
        };
        {
            let mut state = app.state.blocking_write();
            let mut track = crate::state::Track::new("Drums".to_string(), 0.0, 2, 2, 1, 1);
            track.midi.editor_view_mode = crate::message::MidiEditorViewMode::DrumGrid;
            state.tracks.push(track);
        }
        app.save(session_root.to_string_lossy().to_string())
            .expect("save session");

        let session_path = session_root.join("session.json");
        let session: serde_json::Value =
            serde_json::from_reader(File::open(&session_path).expect("open saved session"))
                .expect("parse saved session");
        let tracks = session["tracks"].as_array().expect("tracks array");
        let drum_track = tracks
            .iter()
            .find(|t| t["name"] == "Drums")
            .expect("drums track");
        assert_eq!(
            drum_track["midi"]["editor_view_mode"],
            serde_json::json!("drumgrid")
        );

        fs::remove_dir_all(&session_root).expect("cleanup temp session");
    }

    #[test]
    fn session_load_populates_pending_track_midi_editor_view_mode() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let session_root =
            std::env::temp_dir().join(format!("maolan_midi_view_mode_load_{unique}"));

        let session = serde_json::json!({
            "tracks": [{
                "name": "Drums",
                "level": 0.0,
                "balance": 0.0,
                "armed": false,
                "muted": false,
                "phase_inverted": false,
                "soloed": false,
                "input_monitor": false,
                "disk_monitor": true,
                "frozen": false,
                "height": 100.0,
                "audio": { "clips": [], "ins": 2, "outs": 2 },
                "midi": { "clips": [], "ins": 1, "outs": 1, "editor_view_mode": "drumgrid" },
                "position": { "x": 0.0, "y": 0.0 },
                "automation_mode": "read"
            }],
            "connections": [],
            "transport": {
                "loop_range_samples": null,
                "loop_enabled": false,
                "punch_range_samples": null,
                "punch_enabled": false,
                "sample_rate_hz": 48000,
                "tempo": 120.0,
                "time_signature_num": 4,
                "time_signature_denom": 4,
                "tempo_points": [{"sample": 0, "bpm": 120.0}],
                "time_signature_points": [{"sample": 0, "numerator": 4, "denominator": 4}]
            },
            "ui": {
                "tracks_width": 200.0,
                "mixer_height": 300.0,
                "zoom_visible_bars": 8.0,
                "snap_mode": "bar",
                "midi_snap_mode": "bar"
            },
            "export": {
                "sample_rate_hz": 48000,
                "format_wav": true,
                "format_mp3": false,
                "format_ogg": false,
                "format_flac": false,
                "bit_depth": "int24",
                "mp3_mode": "cbr",
                "mp3_bitrate_kbps": 320,
                "ogg_quality_input": "5",
                "render_mode": "mixdown",
                "hw_out_ports": [],
                "realtime_fallback": false,
                "normalize": false,
                "normalize_mode": "peak",
                "normalize_dbfs_input": "-1.0",
                "normalize_lufs_input": "-14.0",
                "normalize_dbtp_input": "-1.0",
                "normalize_tp_limiter": false,
                "master_limiter": false,
                "master_limiter_ceiling_input": "-1.0"
            },
            "midi_learn_global": {
                "play_pause": null,
                "stop": null,
                "record_toggle": null
            }
        });

        fs::create_dir_all(&session_root).expect("create session dir");
        let session_path = session_root.join("session.json");
        serde_json::to_writer_pretty(
            File::create(&session_path).expect("create session file"),
            &session,
        )
        .expect("write session");

        let mut restored = Maolan {
            ..Maolan::default()
        };
        let _ = restored
            .load(session_root.to_string_lossy().to_string())
            .expect("load session");

        assert!(
            matches!(
                restored.pending_track_midi_editor_view_mode.get("Drums"),
                Some(crate::message::MidiEditorViewMode::DrumGrid)
            ),
            "expected DrumGrid in pending map, got {:?}",
            restored.pending_track_midi_editor_view_mode.get("Drums")
        );

        fs::remove_dir_all(&session_root).expect("cleanup temp session");
    }

    #[test]
    fn cleanup_targets_include_pitchmap_sidecars_only() {
        assert!(Maolan::is_cleanup_target_rel("audio/clip_pitchmap.txt"));
        assert!(Maolan::is_cleanup_target_rel("audio/clip_pitchmap_001.txt"));
        assert!(Maolan::is_cleanup_target_rel("audio/song.wav"));
        assert!(Maolan::is_cleanup_target_rel("midi/pattern.midi"));
        assert!(!Maolan::is_cleanup_target_rel("audio/notes.txt"));
        assert!(!Maolan::is_cleanup_target_rel("audio/../notes.txt"));
        assert!(!Maolan::is_cleanup_target_rel("pitch/cache.txt"));
    }

    #[test]
    fn normalize_session_media_rel_rejects_unsafe_paths_and_normalizes_current_dir() {
        assert_eq!(
            Maolan::normalize_session_media_rel("./audio/clip.wav").as_deref(),
            Some("audio/clip.wav")
        );
        assert_eq!(
            Maolan::normalize_session_media_rel("peaks/clip.json").as_deref(),
            Some("peaks/clip.json")
        );
        assert!(Maolan::normalize_session_media_rel("/tmp/clip.wav").is_none());
        assert!(Maolan::normalize_session_media_rel("../audio/clip.wav").is_none());
    }

    #[test]
    fn file_extension_lower_normalizes_case_and_trims_dot() {
        assert_eq!(
            Maolan::file_extension_lower(std::path::Path::new("Song.MP3")).as_deref(),
            Some("mp3")
        );
        assert_eq!(
            Maolan::file_extension_lower(std::path::Path::new("clip..WAV")).as_deref(),
            Some("wav")
        );
        assert!(Maolan::file_extension_lower(std::path::Path::new("no_extension")).is_none());
    }

    #[test]
    fn import_path_helpers_accept_case_insensitive_extensions() {
        assert!(Maolan::is_import_audio_path(std::path::Path::new(
            "take.FLAC"
        )));
        assert!(Maolan::is_import_audio_path(std::path::Path::new(
            "take.Mp3"
        )));
        assert!(Maolan::is_import_midi_path(std::path::Path::new(
            "pattern.MID"
        )));
        assert!(Maolan::is_import_midi_path(std::path::Path::new(
            "pattern.Midi"
        )));
        assert!(!Maolan::is_import_audio_path(std::path::Path::new(
            "take.txt"
        )));
        assert!(!Maolan::is_import_midi_path(std::path::Path::new(
            "pattern.txt"
        )));
    }

    #[test]
    fn import_track_base_name_trims_and_sanitizes_file_stem() {
        assert_eq!(
            Maolan::import_track_base_name(std::path::Path::new("  Lead Vox!!.wav  ")),
            "Lead_Vox__"
        );
        assert_eq!(
            Maolan::import_track_base_name(std::path::Path::new("   .wav")),
            "clip"
        );
    }

    #[test]
    fn unique_track_name_uses_first_available_numeric_suffix() {
        let mut used = HashSet::from([
            "Track".to_string(),
            "Track_2".to_string(),
            "Track_3".to_string(),
        ]);

        let unique = Maolan::unique_track_name("Track", &mut used);

        assert_eq!(unique, "Track_4");
        assert!(used.contains("Track_4"));
    }

    #[test]
    fn unique_import_rel_path_appends_numeric_suffix_when_name_exists() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let session_root = std::env::temp_dir().join(format!("maolan_test_import_{unique}"));
        fs::create_dir_all(session_root.join("audio")).expect("create temp audio dir");
        fs::write(session_root.join("audio/clip.wav"), b"").expect("seed existing import");

        let rel = Maolan::unique_import_rel_path(&session_root, "audio", "clip", "wav").unwrap();

        assert_eq!(rel, "audio/clip_001.wav");

        fs::remove_dir_all(&session_root).expect("cleanup temp session");
    }

    #[test]
    fn referenced_session_media_paths_include_indirect_and_frozen_assets() {
        let mut state = crate::state::StateData::default();
        let mut track = crate::state::Track::new("Track".to_string(), 0.0, 1, 1, 1, 1);
        let lead_pitch_cache = Maolan::pitch_correction_cache_rel("audio/lead_src.wav", 0, 0);
        let frozen_pitch_cache = Maolan::pitch_correction_cache_rel("audio/frozen_src.wav", 0, 0);
        track.audio.clips.push(crate::state::AudioClip {
            name: "audio/lead.wav".to_string(),
            pitch_correction_source_name: Some("audio/lead_src.wav".to_string()),
            peaks_file: Some("peaks/lead.json".to_string()),
            ..Default::default()
        });
        track.audio.clips.push(crate::state::AudioClip {
            name: "/tmp/external.wav".to_string(),
            ..Default::default()
        });
        track.midi.clips.push(crate::state::MIDIClip {
            name: "midi/pattern.mid".to_string(),
            ..Default::default()
        });
        track.frozen_audio_backup.push(crate::state::AudioClip {
            name: "audio/frozen.wav".to_string(),
            pitch_correction_source_name: Some("audio/frozen_src.wav".to_string()),
            peaks_file: Some("peaks/frozen.json".to_string()),
            ..Default::default()
        });
        track.frozen_midi_backup.push(crate::state::MIDIClip {
            name: "midi/ghost.midi".to_string(),
            ..Default::default()
        });
        track.frozen_render_clip = Some("audio/render.wav".to_string());
        state.tracks.push(track);

        let referenced = Maolan::referenced_session_media_paths(&state);

        assert!(referenced.contains("audio/lead.wav"));
        assert!(referenced.contains("audio/lead_src.wav"));
        assert!(referenced.contains("peaks/lead.json"));
        assert!(referenced.contains(&lead_pitch_cache));
        assert!(referenced.contains("midi/pattern.mid"));
        assert!(referenced.contains("audio/frozen.wav"));
        assert!(referenced.contains("audio/frozen_src.wav"));
        assert!(referenced.contains("peaks/frozen.json"));
        assert!(referenced.contains(&frozen_pitch_cache));
        assert!(referenced.contains("midi/ghost.midi"));
        assert!(referenced.contains("audio/render.wav"));
        assert!(!referenced.contains("/tmp/external.wav"));
    }

    #[test]
    fn referenced_session_media_paths_use_source_offset_and_length_for_pitch_cache_keys() {
        let mut state = crate::state::StateData::default();
        let mut track = crate::state::Track::new("Track".to_string(), 0.0, 1, 1, 1, 1);
        let expected_cache = Maolan::pitch_correction_cache_rel("audio/source.wav", 128, 4096);
        track.audio.clips.push(crate::state::AudioClip {
            name: "audio/rendered.wav".to_string(),
            offset: 12,
            length: 64,
            pitch_correction_source_name: Some("audio/source.wav".to_string()),
            pitch_correction_source_offset: Some(128),
            pitch_correction_source_length: Some(4096),
            ..Default::default()
        });
        state.tracks.push(track);

        let referenced = Maolan::referenced_session_media_paths(&state);

        assert!(referenced.contains("audio/rendered.wav"));
        assert!(referenced.contains("audio/source.wav"));
        assert!(referenced.contains(&expected_cache));
    }

    #[test]
    fn insert_referenced_session_media_path_ignores_unsafe_inputs() {
        let mut referenced = HashSet::new();

        Maolan::insert_referenced_session_media_path(&mut referenced, "/tmp/external.wav");
        Maolan::insert_referenced_session_media_path(&mut referenced, "../audio/clip.wav");
        Maolan::insert_referenced_session_media_path(&mut referenced, "audio/clip.wav");

        assert_eq!(referenced, HashSet::from(["audio/clip.wav".to_string()]));
    }

    #[test]
    fn pitch_correction_cache_rel_is_stable_and_changes_with_segment() {
        let a = Maolan::pitch_correction_cache_rel("audio/source.wav", 0, 1024);
        let b = Maolan::pitch_correction_cache_rel("audio/source.wav", 0, 1024);
        let c = Maolan::pitch_correction_cache_rel("audio/source.wav", 1, 1024);
        let d = Maolan::pitch_correction_cache_rel("audio/source.wav", 0, 2048);

        assert_eq!(a, b);
        assert_ne!(a, c);
        assert_ne!(a, d);
    }

    #[test]
    fn collect_cleanup_candidate_files_recurses_and_skips_referenced_media() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let session_root = std::env::temp_dir().join(format!("maolan_test_cleanup_{unique}"));
        fs::create_dir_all(session_root.join("audio/nested")).expect("create nested audio dir");
        fs::create_dir_all(session_root.join("peaks")).expect("create peaks dir");
        fs::create_dir_all(session_root.join("pitch")).expect("create pitch dir");

        fs::write(session_root.join("audio/keep.wav"), b"").expect("write keep file");
        fs::write(session_root.join("audio/nested/drop_pitchmap.txt"), b"")
            .expect("write nested pitchmap");
        fs::write(session_root.join("peaks/drop.json"), b"").expect("write peak file");
        fs::write(session_root.join("pitch/drop.json"), b"").expect("write pitch cache");
        fs::write(session_root.join("audio/ignore.txt"), b"").expect("write ignored text");

        let referenced = HashSet::from(["audio/keep.wav".to_string()]);
        let mut out = Vec::new();
        Maolan::collect_cleanup_candidate_files(
            &session_root.join("audio"),
            &session_root,
            &referenced,
            &mut out,
        )
        .expect("scan audio");
        Maolan::collect_cleanup_candidate_files(
            &session_root.join("peaks"),
            &session_root,
            &referenced,
            &mut out,
        )
        .expect("scan peaks");
        Maolan::collect_cleanup_candidate_files(
            &session_root.join("pitch"),
            &session_root,
            &referenced,
            &mut out,
        )
        .expect("scan pitch");
        out.sort_by(|a, b| a.1.cmp(&b.1));

        let rels: Vec<_> = out.into_iter().map(|(_, rel)| rel).collect();
        assert_eq!(
            rels,
            vec![
                "audio/nested/drop_pitchmap.txt".to_string(),
                "peaks/drop.json".to_string(),
                "pitch/drop.json".to_string(),
            ]
        );

        fs::remove_dir_all(&session_root).expect("cleanup temp session");
    }

    #[test]
    fn import_prepared_audio_peaks_stores_precomputed_peaks_by_clip_key() {
        let mut app = Maolan::default();
        let peaks = Arc::new(vec![vec![[0.1_f32, 0.4_f32], [-0.2_f32, 0.3_f32]]]);

        let _ = app.update(Message::ImportPreparedAudioPeaks {
            track_name: "Track".to_string(),
            clip_name: "audio/import.wav".to_string(),
            start: 0,
            length: 256,
            offset: 0,
            peaks: peaks.clone(),
        });

        let key = Maolan::audio_clip_key("Track", "audio/import.wav", 0, 256, 0);
        assert_eq!(app.pending_precomputed_peaks.get(&key), Some(&peaks));
    }

    #[test]
    fn drain_audio_peak_updates_applies_chunks_and_clears_pending_rebuilds() {
        let _guard = AUDIO_PEAK_TEST_GUARD.lock().expect("lock audio peak test");
        let mut app = Maolan::default();
        let mut track = crate::state::Track::new("Track".to_string(), 0.0, 1, 1, 0, 0);
        track.audio.clips.push(crate::state::AudioClip {
            name: "audio/import.wav".to_string(),
            start: 0,
            length: 256,
            offset: 0,
            peaks: Arc::new(Vec::new()),
            ..Default::default()
        });
        app.state.blocking_write().tracks.push(track);

        let key = Maolan::audio_clip_key("Track", "audio/import.wav", 0, 256, 0);
        app.pending_peak_rebuilds.insert(key.clone());

        if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
            queue.clear();
            queue.push(AudioPeakChunkUpdate {
                track_name: "Track".to_string(),
                clip_name: "audio/import.wav".to_string(),
                start: 0,
                length: 256,
                offset: 0,
                channels: 1,
                target_bins: 4,
                bin_start: 0,
                peaks: vec![vec![
                    [-0.8_f32, 0.1_f32],
                    [-0.4_f32, 0.3_f32],
                    [-0.2_f32, 0.6_f32],
                    [-0.1_f32, 0.9_f32],
                ]],
                done: false,
            });
            queue.push(AudioPeakChunkUpdate {
                track_name: "Track".to_string(),
                clip_name: "audio/import.wav".to_string(),
                start: 0,
                length: 256,
                offset: 0,
                channels: 1,
                target_bins: 4,
                bin_start: 0,
                peaks: Vec::new(),
                done: true,
            });
        }

        let _ = app.update(Message::DrainAudioPeakUpdates);

        let state = app.state.blocking_read();
        let clip = &state.tracks[0].audio.clips[0];
        assert_eq!(clip.peaks.len(), 1);
        assert_eq!(clip.peaks[0].len(), 4);
        assert_eq!(clip.peaks[0][0], [-0.8_f32, 0.1_f32]);
        assert_eq!(clip.peaks[0][3], [-0.1_f32, 0.9_f32]);
        drop(state);

        assert!(!app.pending_peak_rebuilds.contains(&key));
    }

    #[test]
    fn drain_audio_peak_updates_accumulates_multiple_chunks() {
        let _guard = AUDIO_PEAK_TEST_GUARD.lock().expect("lock audio peak test");
        let mut app = Maolan::default();
        let mut track = crate::state::Track::new("Track".to_string(), 0.0, 1, 1, 0, 0);
        track.audio.clips.push(crate::state::AudioClip {
            name: "audio/import.wav".to_string(),
            start: 0,
            length: 256,
            offset: 0,
            peaks: Arc::new(Vec::new()),
            ..Default::default()
        });
        app.state.blocking_write().tracks.push(track);

        if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
            queue.clear();
            queue.push(AudioPeakChunkUpdate {
                track_name: "Track".to_string(),
                clip_name: "audio/import.wav".to_string(),
                start: 0,
                length: 256,
                offset: 0,
                channels: 1,
                target_bins: 4,
                bin_start: 0,
                peaks: vec![vec![[-0.8_f32, 0.1_f32], [-0.4_f32, 0.3_f32]]],
                done: false,
            });
        }
        let _ = app.update(Message::DrainAudioPeakUpdates);

        if let Ok(mut queue) = AUDIO_PEAK_UPDATES.lock() {
            queue.push(AudioPeakChunkUpdate {
                track_name: "Track".to_string(),
                clip_name: "audio/import.wav".to_string(),
                start: 0,
                length: 256,
                offset: 0,
                channels: 1,
                target_bins: 4,
                bin_start: 2,
                peaks: vec![vec![[-0.2_f32, 0.6_f32], [-0.1_f32, 0.9_f32]]],
                done: false,
            });
            queue.push(AudioPeakChunkUpdate {
                track_name: "Track".to_string(),
                clip_name: "audio/import.wav".to_string(),
                start: 0,
                length: 256,
                offset: 0,
                channels: 1,
                target_bins: 4,
                bin_start: 0,
                peaks: Vec::new(),
                done: true,
            });
        }
        let _ = app.update(Message::DrainAudioPeakUpdates);

        let state = app.state.blocking_read();
        let clip = &state.tracks[0].audio.clips[0];
        assert_eq!(
            clip.peaks.as_ref(),
            &vec![vec![
                [-0.8_f32, 0.1_f32],
                [-0.4_f32, 0.3_f32],
                [-0.2_f32, 0.6_f32],
                [-0.1_f32, 0.9_f32],
            ]]
        );
    }

    #[test]
    fn import_progress_only_finishes_on_last_file_at_full_progress() {
        let mut app = Maolan {
            import_in_progress: true,
            ..Maolan::default()
        };

        let _ = app.update(Message::ImportProgress {
            file_index: 1,
            total_files: 2,
            file_progress: 1.0,
            filename: "first.wav".to_string(),
            operation: None,
        });
        assert!(app.import_in_progress);

        let _ = app.update(Message::ImportProgress {
            file_index: 2,
            total_files: 2,
            file_progress: 0.5,
            filename: "second.wav".to_string(),
            operation: Some("Decoding".to_string()),
        });
        assert!(app.import_in_progress);

        let _ = app.update(Message::ImportProgress {
            file_index: 2,
            total_files: 2,
            file_progress: 1.0,
            filename: "second.wav".to_string(),
            operation: None,
        });
        assert!(!app.import_in_progress);
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn clip_plugin_snapshot_preserves_existing_vst3_state_blob() {
        let previous = json!({
            "plugins": [{
                "format": "VST3",
                "uri": "/tmp/test.vst3",
                "state": {"bytes": [1, 2, 3]}
            }],
            "connections": []
        });
        let plugins = vec![maolan_engine::message::PluginGraphPlugin {
            node: maolan_engine::message::PluginGraphNode::Vst3PluginInstance(0),
            instance_id: 0,
            format: "VST3".to_string(),
            uri: "/tmp/test.vst3".to_string(),
            plugin_id: "plugin-id".to_string(),
            name: "Test".to_string(),
            main_audio_inputs: 2,
            main_audio_outputs: 2,
            audio_inputs: 2,
            audio_outputs: 2,
            midi_inputs: 0,
            midi_outputs: 0,
            state: None,
            bypassed: false,
        }];

        let snapshot = Maolan::plugin_graph_snapshot_to_json(Some(&previous), &plugins, &[]);
        assert_eq!(snapshot["plugins"][0]["state"], json!({"bytes": [1, 2, 3]}));
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn plugin_graph_saved_state_from_json_reads_plugin_slot_by_index() {
        let graph = json!({
            "plugins": [
                {
                    "format": "VST3",
                    "uri": "/plugins/one.vst3",
                    "state": {
                        "plugin_id": "one",
                        "component_state": [1, 2],
                        "controller_state": [3]
                    }
                },
                {
                    "format": "VST3",
                    "uri": "/plugins/two.vst3",
                    "state": {
                        "plugin_id": "two",
                        "component_state": [9],
                        "controller_state": []
                    }
                }
            ],
            "connections": []
        });

        let state = Maolan::plugin_graph_saved_state_from_json::<
            maolan_engine::vst3::Vst3PluginState,
        >(Some(&graph), 1)
        .expect("saved state");

        assert_eq!(state.plugin_id, "two");
        assert_eq!(state.component_state, vec![9]);
        assert!(state.controller_state.is_empty());
    }

    #[cfg(all(unix, not(target_os = "macos")))]
    #[test]
    fn plugin_graph_json_with_saved_plugin_state_updates_only_target_plugin() {
        let graph = json!({
            "plugins": [
                {
                    "format": "VST3",
                    "uri": "/plugins/one.vst3",
                    "state": {
                        "plugin_id": "one",
                        "component_state": [1],
                        "controller_state": []
                    }
                },
                {
                    "format": "VST3",
                    "uri": "/plugins/two.vst3",
                    "state": {
                        "plugin_id": "two",
                        "component_state": [2],
                        "controller_state": []
                    }
                }
            ],
            "connections": []
        });

        let updated = Maolan::plugin_graph_json_with_saved_plugin_state(
            Some(&graph),
            1,
            json!({
                "plugin_id": "two",
                "component_state": [7, 8],
                "controller_state": [6]
            }),
        )
        .expect("updated graph");

        assert_eq!(
            updated["plugins"][0]["state"]["component_state"],
            json!([1])
        );
        assert_eq!(
            updated["plugins"][1]["state"]["component_state"],
            json!([7, 8])
        );
        assert_eq!(
            updated["plugins"][1]["state"]["controller_state"],
            json!([6])
        );
    }

    #[test]
    fn core_toggle_transport_requires_loaded_hw() {
        let mut app = Maolan::default();
        app.state.blocking_write().hw_loaded = false;

        let _ = app.update(Message::ToggleTransport);

        assert!(!app.playing);
        assert!(!app.paused);
    }

    #[test]
    fn core_toggle_transport_stops_active_playback_and_clears_preview() {
        let mut app = Maolan {
            playing: true,
            paused: false,
            recording_preview_start_sample: Some(12),
            recording_preview_sample: Some(24),
            ..Maolan::default()
        };
        app.state.blocking_write().hw_loaded = true;
        app.track_automation_runtime
            .insert("Track".to_string(), TrackAutomationRuntime::default());
        app.touch_automation_overrides
            .insert("Track".to_string(), HashMap::new());
        app.touch_active_keys
            .insert("Track".to_string(), HashSet::new());
        app.latch_automation_overrides
            .insert("Track".to_string(), HashMap::new());

        let _ = app.update(Message::ToggleTransport);

        assert!(!app.playing);
        assert!(!app.paused);
        assert!(app.recording_preview_start_sample.is_none());
        assert!(app.track_automation_runtime.is_empty());
        assert!(app.touch_automation_overrides.is_empty());
        assert!(app.touch_active_keys.is_empty());
        assert!(app.latch_automation_overrides.is_empty());
    }

    #[test]
    fn core_toggle_transport_starts_preview_when_record_armed() {
        let mut app = Maolan {
            record_armed: true,
            transport_samples: 128.0,
            ..Maolan::default()
        };
        app.state.blocking_write().hw_loaded = true;

        let _ = app.update(Message::ToggleTransport);

        assert!(app.playing);
        assert!(!app.paused);
        assert_eq!(app.recording_preview_start_sample, Some(128));
        assert_eq!(app.recording_preview_sample, Some(128));
    }

    #[test]
    fn core_toggle_loop_and_punch_require_ranges() {
        let mut app = Maolan::default();

        let _ = app.update(Message::ToggleLoop);
        let _ = app.update(Message::TogglePunch);
        assert!(!app.loop_enabled);
        assert!(!app.punch_enabled);

        app.loop_range_samples = Some((10, 20));
        app.punch_range_samples = Some((30, 40));
        let _ = app.update(Message::ToggleLoop);
        let _ = app.update(Message::TogglePunch);

        assert!(app.loop_enabled);
        assert!(app.punch_enabled);
    }

    #[test]
    fn simple_ui_mp3_toggle_respects_channel_limits() {
        let mut app = Maolan {
            export_render_mode: ExportRenderMode::Mixdown,
            export_hw_out_ports: BTreeSet::from([0, 1, 2]),
            ..Maolan::default()
        };

        let _ = app.update(Message::ExportFormatMp3Toggled(true));
        assert!(!app.export_format_mp3);

        app.export_hw_out_ports = BTreeSet::from([0, 1]);
        let _ = app.update(Message::ExportFormatMp3Toggled(true));
        assert!(app.export_format_mp3);
    }

    #[test]
    fn simple_ui_render_mode_disables_normalize_and_clamps_mp3() {
        let mut app = Maolan {
            export_format_mp3: true,
            export_normalize: true,
            ..Maolan::default()
        };
        let surround = crate::state::Track::new("Surround".to_string(), 0.0, 1, 4, 0, 0);
        {
            let mut state = app.state.blocking_write();
            state.selected.insert("Surround".to_string());
            state.tracks.push(surround);
        }

        let _ = app.update(Message::ExportRenderModeSelected(
            ExportRenderMode::StemsPostFader,
        ));
        assert!(!app.export_normalize);
        assert!(!app.export_format_mp3);
    }

    #[test]
    fn simple_ui_hw_settings_clamp_numeric_values() {
        let mut app = Maolan::default();

        let _ = app.update(Message::HWSampleRateChanged(0));
        let _ = app.update(Message::HWPeriodFramesChanged(3));
        let _ = app.update(Message::HWNPeriodsChanged(0));
        let _ = app.update(Message::HWSyncModeToggled(true));

        let state = app.state.blocking_read();
        assert_eq!(state.hw_sample_rate_hz, 1);
        assert_eq!(state.oss_period_frames, 16);
        assert_eq!(state.oss_nperiods, 1);
        assert!(state.oss_sync_mode);
    }

    #[test]
    fn track_selection_modifier_messages_require_loaded_hw() {
        let mut app = Maolan::default();
        app.state.blocking_write().hw_loaded = false;

        let _ = app.update(Message::ShiftPressed);
        assert!(!app.state.blocking_read().shift);

        app.state.blocking_write().hw_loaded = true;
        let _ = app.update(Message::ShiftPressed);
        assert!(app.state.blocking_read().shift);
        let _ = app.update(Message::CtrlPressed);
        assert!(app.state.blocking_read().ctrl);
    }

    #[test]
    fn track_selection_select_track_ctrl_adds_to_existing_selection() {
        let mut app = Maolan::default();
        {
            let mut state = app.state.blocking_write();
            state.ctrl = true;
            state.selected.insert("A".to_string());
            state.connection_view_selection =
                crate::state::ConnectionViewSelection::Tracks(HashSet::from(["A".to_string()]));
        }

        let _ = app.update(Message::SelectTrack("B".to_string()));

        let state = app.state.blocking_read();
        assert!(state.selected.contains("A"));
        assert!(state.selected.contains("B"));
        match &state.connection_view_selection {
            crate::state::ConnectionViewSelection::Tracks(set) => {
                assert!(set.contains("A"));
                assert!(set.contains("B"));
            }
            other => panic!("unexpected selection: {other:?}"),
        }
    }

    #[test]
    fn track_selection_double_click_schedules_open_plugins() {
        let mut app = Maolan::default();
        app.state.blocking_write().connections_last_track_click =
            Some(("Track".to_string(), Instant::now()));

        let _ = app.update(Message::SelectTrack("Track".to_string()));
        assert!(
            app.state
                .blocking_read()
                .connections_last_track_click
                .is_none()
        );
    }

    #[test]
    fn session_io_save_folder_selected_none_cancels_pending_exit() {
        let mut app = Maolan {
            pending_exit_after_save: true,
            ..Maolan::default()
        };

        let _ = app.update(Message::SaveFolderSelected(None));

        assert!(!app.pending_exit_after_save);
        assert_eq!(app.state.blocking_read().message, "Close cancelled");
    }

    #[test]
    fn session_io_record_folder_selected_none_clears_pending_record() {
        let mut app = Maolan {
            pending_record_after_save: true,
            ..Maolan::default()
        };

        let _ = app.update(Message::RecordFolderSelected(None));
        assert!(!app.pending_record_after_save);
    }

    #[test]
    fn session_io_open_folder_selected_without_autosave_sets_loading_state() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        let path = std::env::temp_dir().join(format!("maolan_open_session_{unique}"));
        fs::create_dir_all(&path).expect("create session dir");
        let mut app = Maolan {
            recording_preview_start_sample: Some(1),
            recording_preview_sample: Some(2),
            ..Maolan::default()
        };

        let _ = app.update(Message::OpenFolderSelected(Some(path.clone())));
        assert_eq!(app.session_dir.as_ref(), Some(&path));
        assert_eq!(app.state.blocking_read().message, "Loading session...");
        assert!(app.recording_preview_start_sample.is_none());

        fs::remove_dir_all(path).expect("cleanup session dir");
    }

    #[test]
    fn transport_set_loop_and_punch_range_normalize_invalid_ranges() {
        let mut app = Maolan::default();

        let _ = app.update(Message::SetLoopRange(Some((20, 10))));
        let _ = app.update(Message::SetPunchRange(Some((40, 10))));
        assert!(app.loop_range_samples.is_none());
        assert!(app.punch_range_samples.is_none());
        assert!(!app.loop_enabled);
        assert!(!app.punch_enabled);

        let _ = app.update(Message::SetLoopRange(Some((10, 20))));
        let _ = app.update(Message::SetPunchRange(Some((30, 40))));
        assert_eq!(app.loop_range_samples, Some((10, 20)));
        assert_eq!(app.punch_range_samples, Some((30, 40)));
        assert!(app.loop_enabled);
        assert!(app.punch_enabled);
    }

    #[test]
    fn transport_playback_tick_updates_tempo_and_time_signature_inputs() {
        let mut app = Maolan {
            last_sent_tempo_bpm: None,
            last_sent_time_signature: None,
            ..Maolan::default()
        };

        let _ = app.update(Message::PlaybackTick);

        assert_eq!(app.tempo_input, "120.00");
        assert_eq!(app.time_signature_num_input, "4");
        assert_eq!(app.time_signature_denom_input, "4");
        assert_eq!(app.last_sent_tempo_bpm, Some(120.0));
        assert_eq!(app.last_sent_time_signature, Some((4, 4)));
    }

    #[test]
    fn confirm_close_cancel_clears_modal_and_resets_exit_flag() {
        let mut app = Maolan {
            modal: Some(Show::UnsavedChanges),
            pending_exit_after_save: true,
            ..Maolan::default()
        };

        let _ = app.update(Message::ConfirmCloseCancel);

        assert!(app.modal.is_none());
        assert!(!app.pending_exit_after_save);
        assert_eq!(app.state.blocking_read().message, "Close cancelled");
    }

    #[test]
    fn escape_pressed_closes_add_track_modal_and_marker_dialog() {
        let mut app = Maolan {
            modal: Some(Show::AddTrack),
            ..Maolan::default()
        };
        app.state.blocking_write().track_marker_dialog = Some(crate::state::TrackMarkerDialog {
            track_name: "Track".to_string(),
            sample: 10,
            marker_index: None,
            name: "Marker".to_string(),
        });

        let _ = app.update(Message::EscapePressed);
        assert!(app.modal.is_none());
        assert!(app.state.blocking_read().track_marker_dialog.is_some());

        let _ = app.update(Message::EscapePressed);
        assert!(app.state.blocking_read().track_marker_dialog.is_none());
    }

    #[test]
    fn set_snap_mode_updates_state() {
        let mut app = Maolan::default();

        let _ = app.update(Message::SetSnapMode(SnapMode::Sixteenth));

        assert_eq!(app.snap_mode, SnapMode::Sixteenth);
    }

    #[test]
    fn set_midi_snap_mode_updates_state() {
        let mut app = Maolan::default();

        let _ = app.update(Message::SetMidiSnapMode(SnapMode::Beat));

        assert_eq!(app.midi_snap_mode, SnapMode::Beat);
    }

    #[test]
    fn recording_preview_tick_tracks_current_sample_and_respects_punch() {
        let mut app = Maolan {
            playing: true,
            record_armed: true,
            transport_samples: 96.0,
            recording_preview_start_sample: Some(0),
            ..Maolan::default()
        };

        let _ = app.update(Message::RecordingPreviewTick);
        assert_eq!(app.recording_preview_sample, Some(96));

        app.punch_enabled = true;
        app.punch_range_samples = Some((100, 120));
        let _ = app.update(Message::RecordingPreviewTick);
        assert!(app.recording_preview_sample.is_none());
    }

    #[test]
    fn recording_preview_peaks_tick_collects_armed_track_meter_values() {
        let mut app = Maolan {
            playing: true,
            record_armed: true,
            transport_samples: 64.0,
            recording_preview_start_sample: Some(0),
            ..Maolan::default()
        };
        let mut track = crate::state::Track::new("Track".to_string(), 0.0, 1, 2, 0, 0);
        track.armed = true;
        track.meter_out_db = vec![-6.0, -90.0];
        app.state.blocking_write().tracks.push(track);

        let _ = app.update(Message::RecordingPreviewPeaksTick);

        let peaks = app
            .recording_preview_peaks
            .get("Track")
            .expect("preview peaks");
        assert_eq!(peaks.len(), 2);
        assert_eq!(peaks[0].len(), 1);
        assert_eq!(peaks[1].len(), 1);
        assert!(peaks[0][0][1] > 0.49 && peaks[0][0][1] < 0.51);
        assert_eq!(peaks[1][0], [0.0, 0.0]);
    }

    #[test]
    fn zoom_and_scroll_messages_clamp_and_update_positions() {
        let mut app = Maolan {
            size: Size::new(800.0, 600.0),
            zoom_visible_bars: 8.0,
            editor_scroll_origin_samples: 10_000.0,
            ..Maolan::default()
        };

        let _ = app.update(Message::ZoomSliderChanged(0.0));
        assert_eq!(app.zoom_visible_bars, MIN_ZOOM_VISIBLE_BARS);
        assert!(app.editor_scroll_x >= 0.0 && app.editor_scroll_x <= 1.0);

        let _ = app.update(Message::EditorScrollXChanged(2.0));
        assert_eq!(app.editor_scroll_x, 1.0);

        let _ = app.update(Message::EditorScrollYChanged(-1.0));
        assert_eq!(app.editor_scroll_y, 0.0);

        let _ = app.update(Message::MixerScrollXChanged(0.25));
        assert_eq!(app.mixer_scroll_x, 0.25);
    }

    #[test]
    fn piano_scroll_and_zoom_messages_update_state() {
        let mut app = Maolan::default();

        let _ = app.update(Message::PianoZoomXChanged(3.0));
        let _ = app.update(Message::PianoZoomYChanged(2.0));
        let _ = app.update(Message::PianoScrollChanged { x: 1.5, y: -1.0 });
        let _ = app.update(Message::PianoScrollXChanged(0.25));
        let _ = app.update(Message::PianoScrollYChanged(0.75));

        let state = app.state.blocking_read();
        assert_eq!(state.piano_zoom_x, 3.0);
        assert_eq!(state.piano_zoom_y, 2.0);
        assert_eq!(state.piano_scroll_x, 0.25);
        assert_eq!(state.piano_scroll_y, 0.75);
    }

    #[test]
    fn piano_controller_selector_messages_switch_lane_and_sysex_panel() {
        let mut app = Maolan::default();

        let _ = app.update(Message::PianoControllerLaneSelected(
            crate::message::PianoControllerLane::SysEx,
        ));
        {
            let state = app.state.blocking_read();
            assert_eq!(
                state.piano_controller_lane,
                crate::message::PianoControllerLane::SysEx
            );
            assert!(state.piano_sysex_panel_open);
        }

        let _ = app.update(Message::PianoControllerKindSelected(74));
        let _ = app.update(Message::PianoVelocityKindSelected(
            crate::message::PianoVelocityKind::ReleaseVelocity,
        ));
        let _ = app.update(Message::PianoRpnKindSelected(
            crate::message::PianoRpnKind::FineTuning,
        ));
        let _ = app.update(Message::PianoNrpnKindSelected(
            crate::message::PianoNrpnKind::VibratoDepth,
        ));

        let state = app.state.blocking_read();
        assert_eq!(
            state.piano_controller_lane,
            crate::message::PianoControllerLane::Nrpn
        );
        assert_eq!(state.piano_controller_kind, 74);
        assert_eq!(
            state.piano_velocity_kind,
            crate::message::PianoVelocityKind::ReleaseVelocity
        );
        assert_eq!(
            state.piano_rpn_kind,
            crate::message::PianoRpnKind::FineTuning
        );
        assert_eq!(
            state.piano_nrpn_kind,
            crate::message::PianoNrpnKind::VibratoDepth
        );
        assert!(!state.piano_sysex_panel_open);
    }

    #[test]
    fn transport_record_toggle_arms_when_session_exists_and_disarms_when_already_armed() {
        let mut app = Maolan {
            session_dir: Some(PathBuf::from("/tmp/session")),
            playing: true,
            ..Maolan::default()
        };

        let _ = app.update(Message::TransportRecordToggle);
        assert!(app.record_armed);
        assert_eq!(app.recording_preview_start_sample, Some(0));

        let _ = app.update(Message::TransportRecordToggle);
        assert!(!app.record_armed);
        assert!(!app.pending_record_after_save);
        assert!(app.recording_preview_start_sample.is_none());
    }

    #[test]
    fn transport_record_toggle_without_session_sets_pending_record() {
        let mut app = Maolan::default();

        let _ = app.update(Message::TransportRecordToggle);

        assert!(app.pending_record_after_save);
        assert!(!app.record_armed);
    }

    #[cfg(unix)]
    #[test]
    fn resolve_maolan_burn_binary_path_prefers_sibling_binary() {
        let unique = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("time")
            .as_nanos();
        let root = std::env::temp_dir().join(format!("generate-path-test-{unique}"));
        let bin_dir = root.join("target").join("debug");
        fs::create_dir_all(&bin_dir).expect("bin dir");
        let current_exe = bin_dir.join("maolan");
        fs::write(bin_dir.join("maolan-generate"), []).expect("touch sibling");

        let resolved = Maolan::resolve_maolan_burn_binary_path(Some(&current_exe), &root);

        assert_eq!(resolved, Some(bin_dir.join("maolan-generate")));
        let _ = fs::remove_dir_all(root);
    }

    #[cfg(unix)]
    #[test]
    fn resolve_maolan_burn_binary_path_does_not_probe_parent_directory() {
        let unique = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("time")
            .as_nanos();
        let root = std::env::temp_dir().join(format!("generate-parent-test-{unique}"));
        let bin_dir = root.join("target").join("debug");
        fs::create_dir_all(&bin_dir).expect("bin dir");
        let current_exe = bin_dir.join("maolan");
        let parent_candidate = root.join("target").join("maolan-generate");
        fs::write(&parent_candidate, []).expect("touch parent candidate");

        let resolved = Maolan::resolve_maolan_burn_binary_path(Some(&current_exe), &root);

        assert_eq!(resolved, None);
        let _ = fs::remove_dir_all(root);
    }

    #[test]
    fn gui_generate_audio_defaults_match_generate_defaults() {
        let app = Maolan::default();
        assert_eq!(
            app.generate_audio_cfg_scale_input,
            maolan_generate::DEFAULT_CFG_SCALE.to_string()
        );
    }

    #[test]
    fn generate_audio_tags_with_timing_appends_bpm_and_time_signature() {
        assert_eq!(
            Maolan::generate_audio_tags_with_timing("rock, upbeat", 127.6, 7, 8),
            "rock,upbeat,128bpm,7/8 tempo signature"
        );
        assert_eq!(
            Maolan::generate_audio_tags_with_timing("", 120.0, 4, 4),
            "120bpm,4/4 tempo signature"
        );
    }

    #[test]
    fn generate_audio_cfg_scale_input_preserves_decimal_text() {
        let mut app = Maolan::default();
        let _ = app.update(Message::GenerateAudioCfgScaleInput("6.1".to_string()));
        assert_eq!(app.generate_audio_cfg_scale_input, "6.1");
    }

    #[test]
    fn export_format_extension_returns_correct_extensions() {
        assert_eq!(Maolan::export_format_extension(ExportFormat::Wav), "wav");
        assert_eq!(Maolan::export_format_extension(ExportFormat::Mp3), "mp3");
        assert_eq!(Maolan::export_format_extension(ExportFormat::Ogg), "ogg");
        assert_eq!(Maolan::export_format_extension(ExportFormat::Flac), "flac");
    }

    #[test]
    fn sanitize_export_component_replaces_special_chars() {
        assert_eq!(Maolan::sanitize_export_component("test/file"), "test_file");
        assert_eq!(Maolan::sanitize_export_component("test:file"), "test_file");
        assert_eq!(Maolan::sanitize_export_component("test file"), "test_file");
    }

    #[test]
    fn sanitize_peak_file_component_replaces_special_chars() {
        assert_eq!(
            Maolan::sanitize_peak_file_component("track/name"),
            "track_name"
        );
        assert_eq!(Maolan::sanitize_peak_file_component("clip.wav"), "clip_wav");
    }

    #[test]
    fn build_peak_file_rel_creates_correct_path() {
        let rel = Maolan::build_peak_file_rel("TestTrack", 5, "audio.wav");
        assert!(rel.contains("TestTrack"));
        assert!(rel.contains("5"));
        assert!(rel.ends_with(".json"));
    }

    #[test]
    fn audio_clip_key_is_deterministic() {
        let key1 = Maolan::audio_clip_key("track1", "clip.wav", 100, 500, 0);
        let key2 = Maolan::audio_clip_key("track1", "clip.wav", 100, 500, 0);
        assert_eq!(key1, key2);
    }

    #[test]
    fn audio_clip_key_differs_with_params() {
        let key1 = Maolan::audio_clip_key("track1", "clip.wav", 100, 500, 0);
        let key2 = Maolan::audio_clip_key("track1", "clip.wav", 200, 500, 0);
        assert_ne!(key1, key2);
    }

    #[test]
    fn export_base_path_removes_extension() {
        let path = PathBuf::from("/tmp/session/test.wav");
        let base = Maolan::export_base_path(path);
        assert_eq!(base, PathBuf::from("/tmp/session/test"));
    }

    #[test]
    fn sanitize_generated_track_base_name_cleans_input() {
        let sanitized = Maolan::sanitize_generated_track_base_name("My Track with spaces");
        assert!(sanitized.contains("My"));
        assert!(!sanitized.contains(' '));
    }

    #[test]
    fn samples_per_beat_calculation() {
        let app = Maolan::default();
        let spp = app.samples_per_beat();

        assert!(spp > 0.0);
    }

    #[test]
    fn samples_per_bar_calculation() {
        let app = Maolan::default();
        let spb = app.samples_per_bar();

        assert!(spb > 0.0);
    }

    #[test]
    fn zoom_slider_visible_bars_roundtrip() {
        for i in 0..=20 {
            let position = i as f32 / 20.0;
            let visible = zoom_slider_to_visible_bars(position);
            let back = visible_bars_to_zoom_slider(visible);
            assert!(
                (back - position).abs() < 0.001,
                "Roundtrip failed at position {}",
                position
            );
        }
    }

    #[test]
    fn zoom_slider_min_max() {
        assert_eq!(zoom_slider_to_visible_bars(0.0), MIN_ZOOM_VISIBLE_BARS);
        assert_eq!(zoom_slider_to_visible_bars(1.0), MAX_ZOOM_VISIBLE_BARS);
    }

    #[test]
    fn pixels_per_sample_calculation() {
        let app = Maolan::default();
        let pps = app.pixels_per_sample();
        assert!(pps > 0.0);
    }

    #[test]
    fn editor_width_px_calculation() {
        let app = Maolan::default();
        let width = app.editor_width_px();
        assert!(width >= 0.0);
    }

    #[test]
    fn tracks_width_px_calculation() {
        let app = Maolan::default();
        let width = app.tracks_width_px();
        assert!(width >= 0.0);
    }

    #[test]
    fn snap_interval_samples_returns_positive_value() {
        let app = Maolan::default();
        let interval = app.snap_interval_samples();

        assert!(interval > 0);
    }

    #[test]
    fn snap_sample_to_bar_returns_valid_sample() {
        let app = Maolan::default();
        let sample = app.snap_sample_to_bar(1000.0);
        assert!(sample < 10000);
    }

    #[test]
    fn beat_pixels_calculation() {
        let app = Maolan::default();
        let bp = app.beat_pixels();
        assert!(bp > 0.0);
    }
}
