mod generation;
mod model_manager;
mod preferences;
mod projects;
mod view;

pub(crate) use view::app_theme;

use generation::ChatMessage;
use model_manager::{ActiveDownload, ModelDownload, ModelOperation};
use preferences::PreferenceDraft;
#[cfg(test)]
use preferences::{parse_optional_gib, parse_streaming_cache};

use crate::database::{AppPreferences, Database, ProjectWithSessions, SessionState, StoredMessage};
#[cfg(target_os = "macos")]
use crate::engine::ChatTurn;
use crate::metrics::{Metrics, MetricsSnapshot};
use crate::model::{self, DownloadOutcome, DownloadProgress, ManagedArtifactId, ModelChoice};
#[cfg(target_os = "macos")]
use crate::runtime::{ActiveGeneration, CheckpointTarget, GenerationEvent, GenerationService};
use crate::settings::{
    DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences, ReasoningMode,
    RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
    StreamingCacheBudget,
};
use iced::widget::{markdown, scrollable};
use iced::{Size, Subscription, Task, keyboard, window};
use rfd::AsyncFileDialog;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{self, TryRecvError};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, Instant};

const APP_ID: &str = "DS4Server.rfc1437.de";
const METRICS_SAMPLE_INTERVAL: Duration = Duration::from_millis(200);

pub(crate) struct App {
    main_window: window::Id,
    pub(super) model_manager_window: Option<window::Id>,
    pub(super) pending_model_delete: Option<ManagedArtifactId>,
    #[cfg(target_os = "macos")]
    _native_menu: Option<crate::native_menu::NativeMenu>,
    #[cfg(target_os = "macos")]
    pub(super) native_edit_commands: crate::native_edit::EditCommandQueue,
    database: Option<Database>,
    projects: Vec<ProjectWithSessions>,
    preferences: AppPreferences,
    preference_draft: PreferenceDraft,
    preferences_open: bool,
    preference_error: Option<String>,
    selected_project: Option<i32>,
    selected_session: Option<i32>,
    /// Unsaved sessions, keyed by project. A draft only becomes a `sessions` row
    /// when its first chat turn is stored, so empty ones vanish on restart.
    drafts: HashMap<i32, String>,
    /// Session whose quick-actions menu is open.
    session_menu: Option<i32>,
    /// Session being renamed, with the in-progress title.
    session_rename: Option<(i32, String)>,
    /// Projects whose archived sessions are expanded in the sidebar.
    expanded_archives: HashSet<i32>,
    choosing_folder: bool,
    pending_project_path: Option<PathBuf>,
    project_name_input: String,
    model_download: ModelDownload,
    pub(super) composer: String,
    pub(super) conversation: Vec<ChatMessage>,
    pub(super) generating: bool,
    pub(super) context_used: u32,
    pub(super) context_limit: u32,
    pub(super) tokens_per_second: Option<f32>,
    pub(super) detail_tab: DetailTab,
    metrics: Arc<Metrics>,
    pub(super) metrics_snapshot: MetricsSnapshot,
    pub(super) metrics_history: VecDeque<MetricsPoint>,
    last_http_requests: u64,
    #[cfg(target_os = "macos")]
    generation_service: Option<GenerationService>,
    #[cfg(target_os = "macos")]
    active_generation: Option<ActiveGeneration>,
    #[cfg(target_os = "macos")]
    active_titling: Option<generation::TitleRequest>,
    #[cfg(target_os = "macos")]
    runtime_preferences: Arc<RwLock<AppPreferences>>,
    #[cfg(target_os = "macos")]
    _endpoint: Option<crate::server::ServerHandle>,
    error: Option<String>,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) enum DetailTab {
    #[default]
    Chat,
    Stats,
}

#[derive(Clone, Copy, Debug, Default)]
pub(super) struct MetricsPoint {
    pub(super) decode_tokens_per_second: f32,
    pub(super) prefill_tokens_per_second: f32,
    pub(super) http_requests_per_second: f32,
    pub(super) kv_read_bytes_per_second: f32,
    pub(super) kv_write_bytes_per_second: f32,
}

#[derive(Debug, Clone)]
pub(crate) enum Message {
    Noop,
    #[cfg(target_os = "macos")]
    NativeEdit(crate::native_edit::EditCommand),
    OpenPreferences,
    OpenModelManager,
    ModelManagerOpened(window::Id),
    WindowOpened(window::Id),
    WindowClosed(window::Id),
    DismissPanel,
    PreferenceModelChanged(ModelChoice),
    PreferenceDsparkChanged(bool),
    PreferenceTimeoutChanged(String),
    PreferenceEndpointPortChanged(String),
    PreferenceContextChanged(String),
    PreferenceMaxTokensChanged(String),
    PreferenceSystemPromptChanged(String),
    PreferenceTemperatureChanged(String),
    PreferenceTopPChanged(String),
    PreferenceMinPChanged(String),
    PreferenceSeedChanged(String),
    PreferenceReasoningChanged(ReasoningMode),
    PreferenceCpuThreadsChanged(String),
    PreferencePowerChanged(String),
    PreferencePrefillChunkChanged(String),
    PreferenceQualityChanged(bool),
    PreferenceWarmWeightsChanged(bool),
    PreferenceMtpDraftChanged(String),
    PreferenceMtpMarginChanged(String),
    PreferenceGlmMtpChanged(bool),
    PreferenceGlmMtpTimingChanged(bool),
    PreferenceDsparkConfidenceChanged(String),
    PreferenceDsparkStrictChanged(bool),
    PreferenceSsdChanged(bool),
    PreferenceSsdColdChanged(bool),
    PreferenceSsdCacheChanged(String),
    PreferenceSsdFullLayersChanged(String),
    PreferenceSsdPreloadChanged(String),
    PreferenceSteeringFileChanged(String),
    PreferenceSteeringFfnChanged(String),
    PreferenceSteeringAttnChanged(String),
    PreferenceSimulatedMemoryChanged(String),
    PreferenceExpertProfileChanged(String),
    ResetPreferences,
    SavePreferences,
    DownloadArtifact(ManagedArtifactId),
    ValidateArtifact(ManagedArtifactId),
    DeleteArtifact(ManagedArtifactId),
    ConfirmDeleteArtifact,
    CancelDeleteArtifact,
    StopModelDownload,
    DownloadProgressTick,
    ComposerChanged(String),
    ToggleReasoning(usize),
    OpenLink(markdown::Url),
    SubmitPrompt,
    StopGeneration,
    GenerationTick,
    ChooseProjectFolder,
    ProjectFolderPicked(Option<PathBuf>),
    ProjectNameChanged(String),
    ConfirmProject,
    CancelProject,
    SelectProject(i32),
    DeleteProject(i32),
    CreateSession(i32),
    DiscardSession(i32),
    SelectSession(i32, i32),
    DeleteSession(i32),
    OpenSessionMenu(i32),
    StartRenameSession(i32),
    SessionTitleChanged(String),
    ConfirmRenameSession,
    RetitleSession(i32),
    SetSessionState(i32, SessionState),
    ToggleArchivedSessions(i32),
    ShowChat,
    ShowStats,
    MetricsTick,
}

impl App {
    pub(crate) fn load(main_window: window::Id) -> Self {
        let path = application_support_path().join("data.sqlite3");
        match Database::open(&path) {
            Ok(mut database) => match (database.load_projects(), database.load_preferences()) {
                (Ok(projects), Ok(preferences)) => {
                    let preference_draft = match PreferenceDraft::from_saved(&preferences) {
                        Ok(draft) => draft,
                        Err(error) => return Self::failed(error, main_window),
                    };
                    let context_limit = preferences.context_tokens.max(0) as u32;
                    let metrics =
                        Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
                    let metrics_snapshot = metrics.snapshot();
                    #[cfg(target_os = "macos")]
                    let (runtime_preferences, generation_service, endpoint, service_error) =
                        spawn_services(&preferences, Arc::clone(&metrics));
                    Self {
                        main_window,
                        model_manager_window: None,
                        pending_model_delete: None,
                        #[cfg(target_os = "macos")]
                        _native_menu: None,
                        #[cfg(target_os = "macos")]
                        native_edit_commands: crate::native_edit::command_queue(),
                        database: Some(database),
                        projects,
                        preferences,
                        preference_draft,
                        preferences_open: false,
                        preference_error: None,
                        selected_project: None,
                        selected_session: None,
                        drafts: HashMap::new(),
                        session_menu: None,
                        session_rename: None,
                        expanded_archives: HashSet::new(),
                        choosing_folder: false,
                        pending_project_path: None,
                        project_name_input: String::new(),
                        model_download: ModelDownload::Idle,
                        composer: String::new(),
                        conversation: Vec::new(),
                        generating: false,
                        context_used: 0,
                        context_limit,
                        tokens_per_second: None,
                        detail_tab: DetailTab::Chat,
                        metrics,
                        metrics_snapshot,
                        metrics_history: VecDeque::with_capacity(120),
                        last_http_requests: 0,
                        #[cfg(target_os = "macos")]
                        generation_service,
                        #[cfg(target_os = "macos")]
                        active_generation: None,
                        #[cfg(target_os = "macos")]
                        active_titling: None,
                        #[cfg(target_os = "macos")]
                        runtime_preferences,
                        #[cfg(target_os = "macos")]
                        _endpoint: endpoint,
                        error: {
                            #[cfg(target_os = "macos")]
                            {
                                service_error
                            }
                            #[cfg(not(target_os = "macos"))]
                            {
                                None
                            }
                        },
                    }
                }
                (Err(error), _) | (_, Err(error)) => Self::failed(error, main_window),
            },
            Err(error) => Self::failed(error, main_window),
        }
    }

    fn failed(error: String, main_window: window::Id) -> Self {
        let preferences = AppPreferences::default();
        let preference_draft = PreferenceDraft::from_saved(&preferences)
            .expect("default preferences must use a supported model");
        let context_limit = preferences.context_tokens.max(0) as u32;
        let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
        let metrics_snapshot = metrics.snapshot();
        #[cfg(target_os = "macos")]
        let (runtime_preferences, generation_service, endpoint, service_error) =
            spawn_services(&preferences, Arc::clone(&metrics));
        #[cfg(target_os = "macos")]
        let startup_error = service_error;
        #[cfg(not(target_os = "macos"))]
        let startup_error = None::<String>;
        Self {
            main_window,
            model_manager_window: None,
            pending_model_delete: None,
            #[cfg(target_os = "macos")]
            _native_menu: None,
            #[cfg(target_os = "macos")]
            native_edit_commands: crate::native_edit::command_queue(),
            database: None,
            projects: Vec::new(),
            preferences,
            preference_draft,
            preferences_open: false,
            preference_error: None,
            selected_project: None,
            selected_session: None,
            drafts: HashMap::new(),
            session_menu: None,
            session_rename: None,
            expanded_archives: HashSet::new(),
            choosing_folder: false,
            pending_project_path: None,
            project_name_input: String::new(),
            model_download: ModelDownload::Idle,
            composer: String::new(),
            conversation: Vec::new(),
            generating: false,
            context_used: 0,
            context_limit,
            tokens_per_second: None,
            detail_tab: DetailTab::Chat,
            metrics,
            metrics_snapshot,
            metrics_history: VecDeque::with_capacity(120),
            last_http_requests: 0,
            #[cfg(target_os = "macos")]
            generation_service,
            #[cfg(target_os = "macos")]
            active_generation: None,
            #[cfg(target_os = "macos")]
            active_titling: None,
            #[cfg(target_os = "macos")]
            runtime_preferences,
            #[cfg(target_os = "macos")]
            _endpoint: endpoint,
            error: Some(match startup_error {
                Some(service_error) => {
                    format!("Could not open the project database: {error}. {service_error}")
                }
                None => format!("Could not open the project database: {error}"),
            }),
        }
    }

    pub(crate) fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Noop => {}
            #[cfg(target_os = "macos")]
            Message::NativeEdit(command) => {
                crate::native_edit::queue_command(&self.native_edit_commands, command)
            }
            Message::OpenPreferences => self.open_preferences(),
            Message::OpenModelManager => return self.open_model_manager(),
            Message::ModelManagerOpened(id) => {
                if self.model_manager_window == Some(id) {
                    return window::gain_focus(id);
                }
            }
            Message::WindowOpened(id) =>
            {
                #[cfg(target_os = "macos")]
                if id == self.main_window && self._native_menu.is_none() {
                    match crate::native_menu::install() {
                        Ok(menu) => self._native_menu = Some(menu),
                        Err(error) => {
                            self.error =
                                Some(format!("Could not install the application menu: {error}"))
                        }
                    }
                }
            }
            Message::WindowClosed(id) => {
                if id == self.main_window {
                    return iced::exit();
                }
                if self.model_manager_window == Some(id) {
                    self.model_manager_window = None;
                    self.pending_model_delete = None;
                }
            }
            Message::DismissPanel => {
                if self.session_rename.is_some() || self.session_menu.is_some() {
                    self.session_rename = None;
                    self.session_menu = None;
                } else if self.preferences_open {
                    self.preferences_open = false;
                    self.preference_error = None;
                } else if self.pending_project_path.is_some() {
                    self.pending_project_path = None;
                    self.project_name_input.clear();
                    self.error = None;
                }
            }
            Message::ShowChat => self.detail_tab = DetailTab::Chat,
            Message::ShowStats => self.detail_tab = DetailTab::Stats,
            Message::MetricsTick => self.sample_metrics(),
            Message::PreferenceModelChanged(model) => {
                self.preference_draft.model = model;
                if !model.supports_dspark() {
                    self.preference_draft.dspark_enabled = false;
                    self.preference_draft.dspark_confidence_threshold.clear();
                    self.preference_draft.dspark_strict = false;
                }
                if model == ModelChoice::Glm52 {
                    self.preference_draft.power_percent.clear();
                    self.preference_draft.prefill_chunk.clear();
                    self.preference_draft.directional_steering_file.clear();
                    self.preference_draft.directional_steering_ffn.clear();
                    self.preference_draft.directional_steering_attn.clear();
                } else {
                    self.preference_draft.glm_mtp = false;
                    self.preference_draft.glm_mtp_timing = false;
                    self.preference_draft.ssd_full_layers.clear();
                }
                self.preference_error = None;
            }
            Message::PreferenceDsparkChanged(enabled) => {
                self.preference_draft.dspark_enabled =
                    self.preference_draft.model.supports_dspark() && enabled;
                if !self.preference_draft.dspark_enabled {
                    self.preference_draft.dspark_confidence_threshold.clear();
                    self.preference_draft.dspark_strict = false;
                } else {
                    self.preference_draft.ssd_streaming = false;
                }
                self.preference_error = None;
            }
            Message::PreferenceTimeoutChanged(value) => {
                self.preference_draft.idle_timeout_minutes = value;
                self.preference_error = None;
            }
            Message::PreferenceEndpointPortChanged(value) => {
                self.preference_draft.endpoint_port = value;
                self.preference_error = None;
            }
            Message::PreferenceContextChanged(value) => {
                self.preference_draft.context_tokens = value;
                self.preference_error = None;
            }
            Message::PreferenceMaxTokensChanged(value) => {
                self.preference_draft.max_generated_tokens = value;
                self.preference_error = None;
            }
            Message::PreferenceSystemPromptChanged(value) => {
                self.preference_draft.system_prompt = value;
                self.preference_error = None;
            }
            Message::PreferenceTemperatureChanged(value) => {
                self.preference_draft.temperature = value;
                self.preference_error = None;
            }
            Message::PreferenceTopPChanged(value) => {
                self.preference_draft.top_p = value;
                self.preference_error = None;
            }
            Message::PreferenceMinPChanged(value) => {
                self.preference_draft.min_p = value;
                self.preference_error = None;
            }
            Message::PreferenceSeedChanged(value) => {
                self.preference_draft.seed = value;
                self.preference_error = None;
            }
            Message::PreferenceReasoningChanged(value) => {
                self.preference_draft.reasoning_mode = value;
                self.preference_error = None;
            }
            Message::PreferenceCpuThreadsChanged(value) => {
                self.preference_draft.cpu_threads = value;
                self.preference_error = None;
            }
            Message::PreferencePowerChanged(value) => {
                self.preference_draft.power_percent = value;
                self.preference_error = None;
            }
            Message::PreferencePrefillChunkChanged(value) => {
                self.preference_draft.prefill_chunk = value;
                self.preference_error = None;
            }
            Message::PreferenceQualityChanged(value) => {
                self.preference_draft.quality = value;
                self.preference_error = None;
            }
            Message::PreferenceWarmWeightsChanged(value) => {
                self.preference_draft.warm_weights = value;
                self.preference_error = None;
            }
            Message::PreferenceMtpDraftChanged(value) => {
                self.preference_draft.mtp_draft_tokens = value;
                self.preference_error = None;
            }
            Message::PreferenceMtpMarginChanged(value) => {
                self.preference_draft.mtp_margin = value;
                self.preference_error = None;
            }
            Message::PreferenceGlmMtpChanged(value) => {
                self.preference_draft.glm_mtp =
                    self.preference_draft.model == ModelChoice::Glm52 && value;
                if !self.preference_draft.glm_mtp {
                    self.preference_draft.glm_mtp_timing = false;
                }
                self.preference_error = None;
            }
            Message::PreferenceGlmMtpTimingChanged(value) => {
                self.preference_draft.glm_mtp_timing =
                    self.preference_draft.model == ModelChoice::Glm52 && value;
                if self.preference_draft.glm_mtp_timing {
                    self.preference_draft.glm_mtp = true;
                }
                self.preference_error = None;
            }
            Message::PreferenceDsparkConfidenceChanged(value) => {
                self.preference_draft.dspark_confidence_threshold = value;
                if self.preference_draft.model.supports_dspark()
                    && !self
                        .preference_draft
                        .dspark_confidence_threshold
                        .trim()
                        .is_empty()
                {
                    self.preference_draft.dspark_enabled = true;
                    self.preference_draft.ssd_streaming = false;
                }
                self.preference_error = None;
            }
            Message::PreferenceDsparkStrictChanged(value) => {
                self.preference_draft.dspark_strict =
                    self.preference_draft.model.supports_dspark() && value;
                if self.preference_draft.dspark_strict {
                    self.preference_draft.dspark_enabled = true;
                    self.preference_draft.ssd_streaming = false;
                }
                self.preference_error = None;
            }
            Message::PreferenceSsdChanged(value) => {
                self.preference_draft.ssd_streaming = value;
                if value {
                    self.preference_draft.dspark_enabled = false;
                    self.preference_draft.dspark_confidence_threshold.clear();
                    self.preference_draft.dspark_strict = false;
                }
                self.preference_error = None;
            }
            Message::PreferenceSsdColdChanged(value) => {
                self.preference_draft.ssd_streaming_cold = value;
                self.preference_error = None;
            }
            Message::PreferenceSsdCacheChanged(value) => {
                self.preference_draft.ssd_cache = value;
                self.preference_error = None;
            }
            Message::PreferenceSsdFullLayersChanged(value) => {
                if self.preference_draft.model == ModelChoice::Glm52 {
                    self.preference_draft.ssd_full_layers = value;
                }
                self.preference_error = None;
            }
            Message::PreferenceSsdPreloadChanged(value) => {
                self.preference_draft.ssd_preload_experts = value;
                self.preference_error = None;
            }
            Message::PreferenceSteeringFileChanged(value) => {
                if self.preference_draft.model != ModelChoice::Glm52 {
                    self.preference_draft.directional_steering_file = value;
                }
                self.preference_error = None;
            }
            Message::PreferenceSteeringFfnChanged(value) => {
                if self.preference_draft.model != ModelChoice::Glm52 {
                    self.preference_draft.directional_steering_ffn = value;
                }
                self.preference_error = None;
            }
            Message::PreferenceSteeringAttnChanged(value) => {
                if self.preference_draft.model != ModelChoice::Glm52 {
                    self.preference_draft.directional_steering_attn = value;
                }
                self.preference_error = None;
            }
            Message::PreferenceSimulatedMemoryChanged(value) => {
                self.preference_draft.simulated_used_memory_gib = value;
                self.preference_error = None;
            }
            Message::PreferenceExpertProfileChanged(value) => {
                self.preference_draft.expert_profile_path = value;
                self.preference_error = None;
            }
            Message::ResetPreferences => {
                self.preference_draft.reset();
                self.preference_error = None;
            }
            Message::SavePreferences => self.save_preferences(),
            Message::DownloadArtifact(artifact) => {
                self.start_model_operation(artifact, ModelOperation::Download)
            }
            Message::ValidateArtifact(artifact) => {
                self.start_model_operation(artifact, ModelOperation::Validate)
            }
            Message::DeleteArtifact(artifact) => self.pending_model_delete = Some(artifact),
            Message::ConfirmDeleteArtifact => {
                if let Some(artifact) = self.pending_model_delete.take() {
                    match model::delete_managed_artifact(artifact, &models_path()) {
                        Ok(()) => {
                            self.model_download = ModelDownload::Idle;
                            self.error = None;
                        }
                        Err(error) => {
                            self.error = Some(format!("Could not delete {artifact}: {error}"))
                        }
                    }
                }
            }
            Message::CancelDeleteArtifact => self.pending_model_delete = None,
            Message::StopModelDownload => {
                if let ModelDownload::Active(download) = &mut self.model_download {
                    download.stopping = true;
                    download.cancel.store(true, Ordering::Relaxed);
                }
            }
            Message::DownloadProgressTick => self.update_download_progress(),
            Message::ComposerChanged(value) => self.composer = value,
            Message::ToggleReasoning(index) => {
                if let Some(message) = self.conversation.get_mut(index)
                    && message.reasoning.is_some()
                {
                    message.reasoning_open = !message.reasoning_open;
                }
            }
            Message::OpenLink(url) => {
                if matches!(url.scheme(), "http" | "https")
                    && let Err(error) = std::process::Command::new("open").arg(url.as_str()).spawn()
                {
                    self.error = Some(format!("Could not open the link: {error}"));
                }
            }
            Message::SubmitPrompt => {
                self.start_generation();
                return scroll_chat_to_end();
            }
            Message::StopGeneration =>
            {
                #[cfg(target_os = "macos")]
                if let Some(active) = &self.active_generation {
                    active.cancel.store(true, Ordering::Relaxed);
                }
            }
            Message::GenerationTick => {
                #[cfg(target_os = "macos")]
                self.poll_titling();
                if self.poll_generation() {
                    return scroll_chat_to_end();
                }
            }
            Message::ChooseProjectFolder => {
                self.choosing_folder = true;
                return Task::perform(
                    async {
                        AsyncFileDialog::new()
                            .set_title("Choose a project folder")
                            .pick_folder()
                            .await
                            .map(|folder| folder.path().to_path_buf())
                    },
                    Message::ProjectFolderPicked,
                );
            }
            Message::ProjectFolderPicked(path) => {
                self.choosing_folder = false;
                if let Some(path) = path {
                    self.prepare_project(path);
                }
            }
            Message::ProjectNameChanged(value) => self.project_name_input = value,
            Message::ConfirmProject => self.create_project(),
            Message::CancelProject => {
                self.pending_project_path = None;
                self.project_name_input.clear();
                self.error = None;
            }
            Message::SelectProject(project_id) => {
                if self.generating {
                    self.error =
                        Some("Stop the active generation before changing sessions.".into());
                    return Task::none();
                }
                self.selected_project = Some(project_id);
                self.selected_session = None;
                self.conversation.clear();
                self.composer.clear();
                self.context_used = 0;
                self.context_limit = self.preferences.context_tokens.max(0) as u32;
                self.tokens_per_second = None;
                self.error = None;
            }
            Message::DeleteProject(project_id) => {
                if self.generating && self.selected_project == Some(project_id) {
                    self.error =
                        Some("Stop the active generation before deleting its project.".into());
                    return Task::none();
                }
                let checkpoint_ids = self
                    .projects
                    .iter()
                    .find(|item| item.project.id == project_id)
                    .map(|item| {
                        item.sessions
                            .iter()
                            .map(|session| session.id)
                            .collect::<Vec<_>>()
                    })
                    .unwrap_or_default();
                if let Some(database) = &mut self.database {
                    match database.delete_project(project_id) {
                        Ok(()) => {
                            for session_id in checkpoint_ids {
                                let _ = fs::remove_file(session_checkpoint_path(session_id));
                            }
                            self.drafts.remove(&project_id);
                            if self.selected_project == Some(project_id) {
                                self.selected_project = None;
                                self.selected_session = None;
                                self.conversation.clear();
                                self.composer.clear();
                                self.context_used = 0;
                                self.tokens_per_second = None;
                            }
                            self.reload_projects();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            Message::CreateSession(project_id) => self.create_session(project_id),
            Message::DiscardSession(project_id) => self.discard_session(project_id),
            Message::OpenSessionMenu(session_id) => {
                self.session_rename = None;
                self.session_menu = if self.session_menu == Some(session_id) {
                    None
                } else {
                    Some(session_id)
                };
            }
            Message::StartRenameSession(session_id) => {
                let title = self
                    .projects
                    .iter()
                    .flat_map(|project| &project.sessions)
                    .find(|session| session.id == session_id)
                    .map(|session| session.title.clone())
                    .unwrap_or_default();
                self.session_menu = None;
                self.session_rename = Some((session_id, title));
            }
            Message::SessionTitleChanged(title) => {
                if let Some((_, draft)) = &mut self.session_rename {
                    *draft = title;
                }
            }
            Message::ConfirmRenameSession => {
                // Keep the dialog open on failure so the edit is not lost.
                let Some((session_id, title)) = self.session_rename.clone() else {
                    return Task::none();
                };
                if let Some(database) = &mut self.database {
                    match database.rename_session(session_id, &title) {
                        Ok(()) => {
                            self.session_rename = None;
                            self.error = None;
                            self.reload_projects();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            Message::RetitleSession(session_id) => {
                self.session_menu = None;
                #[cfg(target_os = "macos")]
                self.retitle_session(session_id);
                #[cfg(not(target_os = "macos"))]
                {
                    let _ = session_id;
                    self.error = Some("Local Metal generation requires macOS.".into());
                }
            }
            Message::SetSessionState(session_id, state) => {
                self.session_menu = None;
                if let Some(database) = &mut self.database {
                    match database.set_session_state(session_id, state) {
                        Ok(()) => {
                            self.error = None;
                            self.reload_projects();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
            Message::ToggleArchivedSessions(project_id) => {
                if !self.expanded_archives.remove(&project_id) {
                    self.expanded_archives.insert(project_id);
                }
            }
            Message::SelectSession(project_id, session_id) => {
                if self.generating && self.selected_session != Some(session_id) {
                    self.error =
                        Some("Stop the active generation before changing sessions.".into());
                    return Task::none();
                }
                if self.selected_session == Some(session_id) {
                    return Task::none();
                }
                let saved_context = self
                    .projects
                    .iter()
                    .flat_map(|project| &project.sessions)
                    .find(|session| session.id == session_id)
                    .map(|session| {
                        (
                            session.context_used,
                            session.context_limit,
                            session.last_tokens_per_second,
                        )
                    });
                let Some(database) = &mut self.database else {
                    return Task::none();
                };
                match database.load_messages(session_id) {
                    Ok(messages) => {
                        self.conversation = messages.into_iter().map(ChatMessage::from).collect();
                        self.composer.clear();
                        self.selected_project = Some(project_id);
                        self.selected_session = Some(session_id);
                        let (used, limit, tokens_per_second) = saved_context.unwrap_or_default();
                        self.context_used = used.max(0) as u32;
                        self.context_limit = if limit > 0 {
                            limit as u32
                        } else {
                            self.preferences.context_tokens.max(0) as u32
                        };
                        self.tokens_per_second = tokens_per_second;
                        self.error = None;
                        return scroll_chat_to_end();
                    }
                    Err(error) => {
                        self.error = Some(format!("Could not load the chat session: {error}"));
                    }
                }
            }
            Message::DeleteSession(session_id) => {
                if self.generating && self.selected_session == Some(session_id) {
                    self.error =
                        Some("Stop the active generation before deleting its session.".into());
                    return Task::none();
                }
                if let Some(database) = &mut self.database {
                    match database.delete_session(session_id) {
                        Ok(()) => {
                            let _ = fs::remove_file(session_checkpoint_path(session_id));
                            if self.session_menu == Some(session_id) {
                                self.session_menu = None;
                            }
                            if self
                                .session_rename
                                .as_ref()
                                .is_some_and(|(id, _)| *id == session_id)
                            {
                                self.session_rename = None;
                            }
                            if self.selected_session == Some(session_id) {
                                self.selected_session = None;
                                self.conversation.clear();
                                self.composer.clear();
                                self.context_used = 0;
                                self.tokens_per_second = None;
                            }
                            self.reload_projects();
                        }
                        Err(error) => self.error = Some(error),
                    }
                }
            }
        }
        Task::none()
    }

    pub(crate) fn subscription(&self) -> Subscription<Message> {
        let mut subscriptions = vec![
            keyboard::on_key_press(shortcut),
            window::close_requests().map(Message::WindowClosed),
            window::close_events().map(Message::WindowClosed),
        ];
        subscriptions
            .push(iced::time::every(METRICS_SAMPLE_INTERVAL).map(|_| Message::MetricsTick));
        #[cfg(target_os = "macos")]
        subscriptions.push(iced::time::every(Duration::from_millis(50)).map(|_| {
            match crate::native_menu::next_event() {
                Some(crate::native_menu::NativeMenuEvent::Preferences) => Message::OpenPreferences,
                Some(crate::native_menu::NativeMenuEvent::ModelManager) => {
                    Message::OpenModelManager
                }
                Some(crate::native_menu::NativeMenuEvent::Edit(command)) => {
                    Message::NativeEdit(command)
                }
                None => Message::Noop,
            }
        }));
        if matches!(self.model_download, ModelDownload::Active(_)) {
            subscriptions.push(
                iced::time::every(Duration::from_secs(1)).map(|_| Message::DownloadProgressTick),
            );
        }
        #[cfg(target_os = "macos")]
        let titling = self.active_titling.is_some();
        #[cfg(not(target_os = "macos"))]
        let titling = false;
        if self.generating || titling {
            subscriptions.push(
                iced::time::every(Duration::from_millis(50)).map(|_| Message::GenerationTick),
            );
        }
        Subscription::batch(subscriptions)
    }

    pub(crate) fn title(&self, id: window::Id) -> String {
        if self.model_manager_window == Some(id) {
            "Model Manager — DS4Server".to_owned()
        } else {
            "DS4Server".to_owned()
        }
    }

    fn sample_metrics(&mut self) {
        let prefill_tokens_per_second = self.metrics.take_prefill_sample();
        let (kv_read_bytes, kv_write_bytes) = self.metrics.take_kv_io_sample();
        let snapshot = self.metrics.snapshot();
        let http_requests_per_second = snapshot
            .http_requests
            .saturating_sub(self.last_http_requests) as f32
            / METRICS_SAMPLE_INTERVAL.as_secs_f32();
        let kv_read_bytes_per_second = kv_read_bytes as f32 / METRICS_SAMPLE_INTERVAL.as_secs_f32();
        let kv_write_bytes_per_second =
            kv_write_bytes as f32 / METRICS_SAMPLE_INTERVAL.as_secs_f32();
        self.last_http_requests = snapshot.http_requests;
        self.metrics_history.push_back(MetricsPoint {
            decode_tokens_per_second: if snapshot.phase == crate::metrics::RuntimePhase::Generating
            {
                snapshot.decode_tokens_per_second
            } else {
                0.0
            },
            prefill_tokens_per_second,
            http_requests_per_second,
            kv_read_bytes_per_second,
            kv_write_bytes_per_second,
        });
        if self.metrics_history.len() > 120 {
            self.metrics_history.pop_front();
        }
        self.metrics_snapshot = snapshot;
    }
}

#[cfg(target_os = "macos")]
fn spawn_services(
    preferences: &AppPreferences,
    metrics: Arc<Metrics>,
) -> (
    Arc<RwLock<AppPreferences>>,
    Option<GenerationService>,
    Option<crate::server::ServerHandle>,
    Option<String>,
) {
    let runtime_preferences = Arc::new(RwLock::new(preferences.clone()));
    let generation = match GenerationService::spawn(Arc::clone(&metrics)) {
        Ok(generation) => generation,
        Err(error) => return (runtime_preferences, None, None, Some(error)),
    };
    let endpoint = crate::server::ServerHandle::spawn(
        generation.clone(),
        Arc::clone(&runtime_preferences),
        models_path(),
        transient_cache_path(),
        u16::try_from(preferences.endpoint_port).unwrap_or(4000),
        metrics,
    );
    match endpoint {
        Ok(endpoint) => (runtime_preferences, Some(generation), Some(endpoint), None),
        Err(error) => (runtime_preferences, Some(generation), None, Some(error)),
    }
}

impl Drop for App {
    fn drop(&mut self) {
        if let ModelDownload::Active(download) = &self.model_download {
            download.cancel.store(true, Ordering::Relaxed);
        }
        #[cfg(target_os = "macos")]
        if let Some(active) = &self.active_generation {
            active.cancel.store(true, Ordering::Relaxed);
        }
    }
}

fn shortcut(key: keyboard::Key, modifiers: keyboard::Modifiers) -> Option<Message> {
    match key.as_ref() {
        keyboard::Key::Character(",") if modifiers.command() => Some(Message::OpenPreferences),
        keyboard::Key::Character("m") if modifiers.command() && modifiers.shift() => {
            Some(Message::OpenModelManager)
        }
        keyboard::Key::Named(keyboard::key::Named::Escape) => Some(Message::DismissPanel),
        _ => None,
    }
}

fn application_support_path() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."))
        .join("Library")
        .join("Application Support")
        .join(APP_ID)
}

fn models_path() -> PathBuf {
    application_support_path().join("models")
}

pub(super) fn chat_scroll_id() -> scrollable::Id {
    scrollable::Id::new("chat-transcript")
}

fn scroll_chat_to_end() -> Task<Message> {
    scrollable::snap_to(chat_scroll_id(), scrollable::RelativeOffset::END)
}

fn session_checkpoint_path(session_id: i32) -> PathBuf {
    application_support_path()
        .join("kv-cache")
        .join(format!("{session_id}.bin"))
}

/// Content-addressed KV cache for turns that belong to no stored session: the
/// HTTP endpoint and the app's own one-shot requests share it.
fn transient_cache_path() -> PathBuf {
    application_support_path().join("kv-cache").join("http")
}

pub(crate) fn app_icon() -> window::Icon {
    let decoder = png::Decoder::new(std::io::Cursor::new(include_bytes!(
        "../assets/app-icon.png"
    )));
    let mut reader = decoder
        .read_info()
        .expect("bundled application icon must be valid PNG");
    let mut rgba = vec![0; reader.output_buffer_size()];
    let info = reader
        .next_frame(&mut rgba)
        .expect("bundled application icon must decode");
    assert_eq!(info.color_type, png::ColorType::Rgba);
    assert_eq!(info.bit_depth, png::BitDepth::Eight);
    rgba.truncate(info.buffer_size());
    window::icon::from_rgba(rgba, info.width, info.height)
        .expect("bundled application icon dimensions must be valid")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn preferences_shortcut_and_dspark_support_are_explicit() {
        let message = shortcut(
            keyboard::Key::Character(",".into()),
            keyboard::Modifiers::COMMAND,
        );
        assert!(matches!(message, Some(Message::OpenPreferences)));
        let message = shortcut(
            keyboard::Key::Character("m".into()),
            keyboard::Modifiers::COMMAND | keyboard::Modifiers::SHIFT,
        );
        assert!(matches!(message, Some(Message::OpenModelManager)));
        assert!(ModelChoice::DeepSeekV4Flash.supports_dspark());
        assert!(!ModelChoice::DeepSeekV4Pro.supports_dspark());
        assert!(!ModelChoice::Glm52.supports_dspark());
    }

    #[test]
    fn ds4_gib_and_streaming_cache_inputs_are_typed() {
        assert_eq!(
            parse_streaming_cache("128").unwrap(),
            Some(StreamingCacheBudget::Experts(128))
        );
        assert_eq!(
            parse_streaming_cache("64gB").unwrap(),
            Some(StreamingCacheBudget::Gib(64))
        );
        assert!(parse_streaming_cache("1.5GB").is_err());
        assert_eq!(parse_optional_gib("Memory", "8GB").unwrap(), Some(8));
    }

    #[test]
    fn one_shot_titles_are_reduced_to_a_single_sidebar_line() {
        use generation::session_title;
        assert_eq!(
            session_title("\"Metal kernel debugging\"").as_deref(),
            Some("Metal kernel debugging")
        );
        assert_eq!(
            session_title("Fixing the tokenizer.\nThat is my answer.").as_deref(),
            Some("Fixing the tokenizer")
        );
        assert_eq!(session_title("   \n  \n").as_deref(), None);
        let long = "word ".repeat(40);
        let title = session_title(&long).expect("a long reply still yields a title");
        assert!(title.chars().count() <= 61, "{title}");
        assert!(title.ends_with('…'));
    }

    #[test]
    fn assistant_stream_keeps_reasoning_separate_from_the_answer() {
        let mut message = ChatMessage {
            id: 1,
            user: false,
            reasoning: Some(String::new()),
            reasoning_complete: false,
            reasoning_open: true,
            content: String::new(),
            markdown: Vec::new(),
        };
        message.append(true, "working it out");
        message.append(false, "**final answer**");
        message.refresh_markdown();
        assert_eq!(message.reasoning.as_deref(), Some("working it out"));
        assert!(message.reasoning_complete);
        assert_eq!(message.content, "**final answer**");
        assert!(!message.markdown.is_empty());
    }
}
