use super::*;

impl App {
    pub(super) fn prepare_project(&mut self, path: PathBuf) {
        let Ok(path) = fs::canonicalize(path) else {
            self.error = Some("The selected folder is no longer available.".into());
            return;
        };
        let Some(path_text) = path.to_str() else {
            self.error = Some("The selected folder path is not valid UTF-8.".into());
            return;
        };
        if self
            .projects
            .iter()
            .any(|item| item.project.path == path_text)
        {
            self.error = Some("That project is already in the sidebar.".into());
            return;
        }

        self.project_name_input = path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("Project")
            .to_owned();
        self.pending_project_path = Some(path);
        self.error = None;
    }

    pub(super) fn create_project(&mut self) {
        let name = self.project_name_input.trim();
        if name.is_empty() {
            self.error = Some("Project name cannot be empty.".into());
            return;
        }
        let Some(path) = &self.pending_project_path else {
            return;
        };
        let Some(path) = path.to_str() else {
            self.error = Some("The selected folder path is not valid UTF-8.".into());
            return;
        };
        let Some(database) = &mut self.database else {
            return;
        };

        match database.create_project(name, path) {
            Ok(project) => {
                self.remember_project(project.id);
                self.selected_session = None;
                self.pending_project_path = None;
                self.project_name_input.clear();
                self.error = None;
                self.reload_projects();
            }
            Err(error) => self.error = Some(error),
        }
    }

    /// Opens an unsaved session on a project and selects it. Nothing reaches the
    /// database until the first chat turn is stored by [`App::persist_session`].
    pub(super) fn create_session(&mut self, project_id: i32) {
        if self.generating {
            self.error = Some("Stop the active generation before creating a session.".into());
            return;
        }
        if self.database.is_none() {
            return;
        }
        let title = draft_title(&self.projects, project_id);
        self.drafts.entry(project_id).or_insert(title);
        self.remember_project(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;
    }

    pub(super) fn discard_session(&mut self, project_id: i32) {
        if self.drafts.remove(&project_id).is_some() && self.draft_selected(project_id) {
            self.conversation.clear();
            self.composer.clear();
            self.context_used = 0;
            self.tokens_per_second = None;
        }
        self.error = None;
    }

    /// Turns the draft on `project_id` into a real session row. Called when the
    /// first chat turn is about to be written, never before.
    pub(super) fn persist_session(&mut self, project_id: i32) -> Result<i32, String> {
        let title = self
            .drafts
            .get(&project_id)
            .cloned()
            .unwrap_or_else(|| draft_title(&self.projects, project_id));
        let database = self
            .database
            .as_mut()
            .ok_or_else(|| "The project database is unavailable.".to_owned())?;
        let session = database.create_session(project_id, &title)?;
        self.drafts.remove(&project_id);
        self.remember_project(project_id);
        self.selected_session = Some(session.id);
        self.reload_projects();
        Ok(session.id)
    }

    /// Selects a project and stores it as the one to reopen on the next launch.
    pub(super) fn remember_project(&mut self, project_id: i32) {
        self.selected_project = Some(project_id);
        if self.preferences.last_project_id == Some(project_id) {
            return;
        }
        self.preferences.last_project_id = Some(project_id);
        if let Some(database) = &mut self.database
            && let Err(error) = database.set_last_project(Some(project_id))
        {
            self.error = Some(error);
        }
    }

    /// True when the sidebar row for this project's draft is the active chat.
    pub(super) fn draft_selected(&self, project_id: i32) -> bool {
        self.selected_project == Some(project_id) && self.selected_session.is_none()
    }

    pub(super) fn reload_projects(&mut self) {
        if let Some(database) = &mut self.database {
            match database.load_projects() {
                Ok(projects) => {
                    self.projects = projects;
                    if super::sweep_orphan_checkpoints(&super::kv_cache_path(), &self.projects) {
                        self.finish_cache_change();
                    }
                }
                Err(error) => self.error = Some(error),
            }
        }
    }
}

pub(super) fn draft_title(projects: &[ProjectWithSessions], project_id: i32) -> String {
    let stored = projects
        .iter()
        .find(|item| item.project.id == project_id)
        .map_or(0, |item| item.sessions.len());
    format!("Session {}", stored + 1)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::database::{Project, Session};

    #[test]
    fn session_states_are_exclusive_and_ordered() {
        assert_eq!(SessionState::from_id("pinned"), Some(SessionState::Pinned));
        assert_eq!(SessionState::from_id("nonsense"), None);
        assert_eq!(SessionState::default(), SessionState::Normal);
        let mut ranks = [
            SessionState::Archived,
            SessionState::Normal,
            SessionState::Pinned,
        ];
        ranks.sort_by_key(|state| state.rank());
        assert_eq!(
            ranks,
            [
                SessionState::Pinned,
                SessionState::Normal,
                SessionState::Archived
            ]
        );
    }

    #[test]
    fn draft_titles_follow_the_stored_session_count() {
        let projects = vec![
            ProjectWithSessions {
                project: project(1, "First"),
                sessions: vec![session(1, 1), session(2, 1)],
            },
            ProjectWithSessions {
                project: project(2, "Second"),
                sessions: Vec::new(),
            },
        ];
        assert_eq!(draft_title(&projects, 1), "Session 3");
        assert_eq!(draft_title(&projects, 2), "Session 1");
        assert_eq!(draft_title(&projects, 99), "Session 1");
    }

    #[test]
    fn sweeping_removes_checkpoints_of_sessions_that_no_longer_exist() {
        let directory =
            std::env::temp_dir().join(format!("ds4-server-sweep-{}", std::process::id()));
        std::fs::create_dir_all(&directory).unwrap();
        for name in ["1.bin", "2.bin", "notes.bin", "7.bin"] {
            std::fs::write(directory.join(name), b"payload").unwrap();
        }
        let projects = vec![ProjectWithSessions {
            project: project(1, "First"),
            sessions: vec![session(1, 1), session(2, 1)],
        }];

        assert!(super::super::sweep_orphan_checkpoints(
            &directory, &projects
        ));

        assert!(directory.join("1.bin").exists());
        assert!(directory.join("2.bin").exists());
        // Not a session checkpoint, so not ours to delete.
        assert!(directory.join("notes.bin").exists());
        assert!(!directory.join("7.bin").exists());
        // Nothing left to sweep on the next pass.
        assert!(!super::super::sweep_orphan_checkpoints(
            &directory, &projects
        ));

        std::fs::remove_dir_all(directory).unwrap();
    }

    fn project(id: i32, name: &str) -> Project {
        Project {
            id,
            name: name.into(),
            path: format!("/tmp/{name}"),
            collapsed: false,
        }
    }

    fn session(id: i32, project_id: i32) -> Session {
        Session::fixture(
            id,
            project_id,
            &format!("Session {id}"),
            SessionState::Normal,
        )
    }
}
