//! Asset management — walkdir-based discovery of static files.
//!
//! Provides a lightweight `AssetManager` for listing and locating files under
//! a base directory.  Filesystem I/O is sync; intended for server-side use
//! only (build-time or SSR startup).

use std::path::{Path, PathBuf};

use thiserror::Error;
use walkdir::WalkDir;

#[derive(Debug, Error)]
pub enum AssetError {
    #[error("Asset directory not found: {0}")]
    NotFound(PathBuf),
    #[error("Directory traversal error: {0}")]
    WalkDir(#[from] walkdir::Error),
    #[error("Metadata read error for {path}: {source}")]
    Metadata {
        path: PathBuf,
        source: std::io::Error,
    },
}

pub type Result<T> = std::result::Result<T, AssetError>;

/// Metadata for a discovered asset file.
#[derive(Debug, Clone)]
pub struct AssetInfo {
    /// Absolute path on disk.
    pub path: PathBuf,
    /// Path relative to the `AssetManager` base directory.
    pub relative: String,
    /// File extension without leading dot, if any.
    pub extension: Option<String>,
    /// File size in bytes.
    pub size: u64,
}

/// Discovers and resolves static assets under a base directory.
pub struct AssetManager {
    base_dir: PathBuf,
}

impl AssetManager {
    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
        Self {
            base_dir: base_dir.into(),
        }
    }

    /// Absolute path for a relative asset name.
    pub fn asset_path(&self, relative: &str) -> PathBuf {
        self.base_dir.join(relative)
    }

    /// Returns `true` if the asset exists on disk.
    pub fn exists(&self, relative: &str) -> bool {
        self.asset_path(relative).exists()
    }

    /// List all files under the base directory.
    ///
    /// `extension_filter` — when `Some("html")`, only files with that extension
    /// are returned.  Pass `None` to list everything.
    pub fn list(&self, extension_filter: Option<&str>) -> Result<Vec<AssetInfo>> {
        if !self.base_dir.exists() {
            return Err(AssetError::NotFound(self.base_dir.clone()));
        }

        let mut assets = Vec::new();

        for entry in WalkDir::new(&self.base_dir)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().is_file())
        {
            if let Some(ext_filter) = extension_filter {
                let has_ext = entry
                    .path()
                    .extension()
                    .and_then(|s| s.to_str())
                    .map(|s| s == ext_filter)
                    .unwrap_or(false);
                if !has_ext {
                    continue;
                }
            }

            let relative = entry
                .path()
                .strip_prefix(&self.base_dir)
                .map(|p| p.to_string_lossy().into_owned())
                .unwrap_or_default();

            let size = entry
                .metadata()
                .map_err(|e| AssetError::Metadata {
                    path: entry.path().to_owned(),
                    source: e.into(),
                })?
                .len();

            assets.push(AssetInfo {
                path: entry.path().to_owned(),
                relative,
                extension: entry
                    .path()
                    .extension()
                    .and_then(|s| s.to_str())
                    .map(|s| s.to_owned()),
                size,
            });
        }

        Ok(assets)
    }

    /// List all Markdown files (`*.md`) under the base directory.
    pub fn list_markdown(&self) -> Result<Vec<AssetInfo>> {
        self.list(Some("md"))
    }

    /// List all FTL files (`*.ftl`) under the base directory.
    pub fn list_ftl(&self) -> Result<Vec<AssetInfo>> {
        self.list(Some("ftl"))
    }
}

impl AssetManager {
    /// Returns the base directory path.
    pub fn base_dir(&self) -> &Path {
        &self.base_dir
    }
}

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

    #[test]
    fn nonexistent_dir_returns_not_found() {
        let mgr = AssetManager::new("/tmp/__rustelo_content_test_nonexistent__");
        let err = mgr.list(None).unwrap_err();
        assert!(matches!(err, AssetError::NotFound(_)));
    }

    #[test]
    fn exists_returns_false_for_missing_asset() {
        let mgr = AssetManager::new("/tmp");
        assert!(!mgr.exists("__no_such_file__.md"));
    }
}
