//! Project I/O — the ONLY place in `service/project/` that may perform
//! filesystem reads, state mutations, or store operations.
//!
//! Every function here is a thin adapter that converts lower-level errors
//! into the typed [`super::Error`] for this domain. No business logic lives
//! here; branching belongs in `selection.rs` or `mod.rs`.

use crate::state::SessionState;
use crate::store::StoreOps;
use std::path::{Path, PathBuf};

use super::Error;

const KEY_LAST_PROJECT: &str = "last_project_path";
const KEY_KNOWN_PROJECTS: &str = "known_projects";

/// Validate that `path` exists and is a directory, returning a typed error.
pub(super) async fn validate_path(path: &Path) -> Result<(), Error> {
    let p = path.to_path_buf();
    tokio::task::spawn_blocking(move || {
        if !p.exists() {
            return Err(Error::PathNotFound(format!(
                "Path does not exist: {}",
                p.display()
            )));
        }
        if !p.is_dir() {
            return Err(Error::NotADirectory(format!(
                "Path is not a directory: {}",
                p.display()
            )));
        }
        Ok(())
    })
    .await
    .map_err(|e| Error::Internal(format!("Task failed: {e}")))?
}

/// Ensure the project directory has a `.huskies/` scaffold and an `.mcp.json`.
///
/// Creates the directory if it does not exist. If `.huskies/` is absent,
/// writes the full scaffold. Always rewrites `.mcp.json` with `port`.
pub(super) async fn ensure_scaffold(path: PathBuf, port: u16) -> Result<(), Error> {
    crate::io::fs::project::ensure_project_root_with_story_kit(path, port)
        .await
        .map_err(Error::Internal)
}

/// Set (or clear) the active project root in session state.
pub(super) fn set_project_root(state: &SessionState, path: Option<PathBuf>) -> Result<(), Error> {
    // TRACE:MERGE-DEBUG — remove once root cause is found
    match &path {
        Some(p) => crate::slog!(
            "[MERGE-DEBUG] open_project: setting project_root to {:?}",
            p
        ),
        None => crate::slog!("[MERGE-DEBUG] close_project: setting project_root to None"),
    }
    let mut root = state
        .project_root
        .lock()
        .map_err(|e| Error::Internal(format!("Lock poisoned: {e}")))?;
    *root = path;
    Ok(())
}

/// Read the active project root from session state.
pub(super) fn get_project_root_from_state(state: &SessionState) -> Result<Option<PathBuf>, Error> {
    let root = state
        .project_root
        .lock()
        .map_err(|e| Error::Internal(format!("Lock poisoned: {e}")))?;
    Ok(root.clone())
}

/// Persist the last-used project path and known-projects list to the store.
///
/// Sets both keys and flushes in a single `save()` call to minimise writes.
pub(super) fn persist_open_project(
    path: &str,
    known: &[String],
    store: &dyn StoreOps,
) -> Result<(), Error> {
    store.set(KEY_LAST_PROJECT, serde_json::json!(path));
    store.set(KEY_KNOWN_PROJECTS, serde_json::json!(known));
    store.save().map_err(Error::Internal)
}

/// Remove the persisted project path from the store and flush.
pub(super) fn clear_project(store: &dyn StoreOps) -> Result<(), Error> {
    store.delete(KEY_LAST_PROJECT);
    store.save().map_err(Error::Internal)
}

/// Read the known-projects list from the store.
pub(super) fn read_known_projects(store: &dyn StoreOps) -> Vec<String> {
    store
        .get(KEY_KNOWN_PROJECTS)
        .and_then(|val| val.as_array().cloned())
        .unwrap_or_default()
        .into_iter()
        .filter_map(|val| val.as_str().map(|s| s.to_string()))
        .collect()
}

/// Persist the known-projects list to the store and flush.
pub(super) fn save_known_projects(projects: &[String], store: &dyn StoreOps) -> Result<(), Error> {
    store.set(KEY_KNOWN_PROJECTS, serde_json::json!(projects));
    store.save().map_err(Error::Internal)
}

/// Try to restore the project root from the persisted store path.
///
/// If the stored path still exists and is a directory, updates session state
/// and returns the path string. Returns `Ok(None)` when no valid stored path
/// is found.
pub(super) fn restore_from_store(
    state: &SessionState,
    store: &dyn StoreOps,
) -> Result<Option<String>, Error> {
    let last = store
        .get(KEY_LAST_PROJECT)
        .and_then(|val| val.as_str().map(|s| s.to_string()));

    if let Some(path_str) = last {
        let p = PathBuf::from(&path_str);
        if p.exists() && p.is_dir() {
            // TRACE:MERGE-DEBUG — remove once root cause is found
            crate::slog!(
                "[MERGE-DEBUG] get_current_project: project_root was None, \
                 restoring from store to {:?}",
                p
            );
            let mut root = state
                .project_root
                .lock()
                .map_err(|e| Error::Internal(format!("Lock poisoned: {e}")))?;
            *root = Some(p);
            return Ok(Some(path_str));
        }
    }

    Ok(None)
}
