//! MATLAB-compatible `ls` builtin for RunMat.

use runmat_filesystem as vfs;
use std::collections::HashSet;
#[cfg(test)]
use std::env;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

use glob::glob;
use runmat_builtins::{CharArray, StringArray, Value};
use runmat_macros::runtime_builtin;

use crate::builtins::common::fs::{
    contains_wildcards, expand_user_path, path_to_string, sort_entries,
};
use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::console::{record_console_line, ConsoleStream};
use crate::output_context::requested_output_count;
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::ls")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "ls",
    op_kind: GpuOpKind::Custom("io"),
    supported_precisions: &[],
    broadcast: BroadcastSemantics::None,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::GatherImmediately,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes:
        "Host-only filesystem builtin. Providers do not participate; any GPU-resident argument is gathered before path expansion.",
};

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::ls")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "ls",
    shape: ShapeRequirements::Any,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes: "I/O builtins are excluded from fusion plans; metadata registered for introspection completeness.",
};

const BUILTIN_NAME: &str = "ls";

fn ls_error(message: impl Into<String>) -> RuntimeError {
    build_runtime_error(message)
        .with_builtin(BUILTIN_NAME)
        .build()
}

fn map_control_flow(err: RuntimeError) -> RuntimeError {
    let identifier = err.identifier().map(str::to_string);
    let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
        .with_builtin(BUILTIN_NAME)
        .with_source(err);
    if let Some(identifier) = identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

#[runtime_builtin(
    name = "ls",
    category = "io/repl_fs",
    summary = "List files and folders in the current directory or matching a wildcard pattern.",
    keywords = "ls,list files,folder contents,wildcard listing,dir",
    accel = "cpu",
    suppress_auto_output = true,
    type_resolver(crate::builtins::io::type_resolvers::ls_type),
    builtin_path = "crate::builtins::io::repl_fs::ls"
)]
async fn ls_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
    let gathered = gather_arguments(&args).await?;
    if gathered.len() > 1 {
        return Err(ls_error("ls: too many input arguments"));
    }

    let entries = if let Some(value) = gathered.first() {
        list_from_value(value).await?
    } else {
        list_current_directory().await?
    };

    if should_emit_stdout() {
        emit_listing_stdout(&entries);
    }
    rows_to_char_array(&entries)
}

async fn list_from_value(value: &Value) -> BuiltinResult<Vec<String>> {
    let names = patterns_from_value(value)?;
    if names.is_empty() {
        return list_current_directory().await;
    }

    let mut seen = HashSet::new();
    let mut combined = Vec::new();

    for pattern in names {
        let matches = list_for_pattern(&pattern).await?;
        for entry in matches {
            if seen.insert(entry.clone()) {
                combined.push(entry);
            }
        }
    }

    sort_entries(&mut combined);
    Ok(combined)
}

async fn list_current_directory() -> BuiltinResult<Vec<String>> {
    let cwd = vfs::current_dir()
        .map_err(|err| ls_error(format!("ls: unable to determine current directory ({err})")))?;
    list_directory(&cwd).await
}

async fn list_for_pattern(raw: &str) -> BuiltinResult<Vec<String>> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return list_current_directory().await;
    }

    let expanded = expand_user_path(trimmed, "ls").map_err(ls_error)?;

    if contains_wildcards(&expanded) {
        list_glob_pattern(&expanded, trimmed).await
    } else {
        list_path(&expanded, trimmed).await
    }
}

async fn list_directory(dir: &Path) -> BuiltinResult<Vec<String>> {
    let mut entries = Vec::new();
    let dir_str = path_to_string(dir);
    let read_dir = vfs::read_dir_async(dir)
        .await
        .map_err(|err| ls_error(format!("ls: unable to access '{dir_str}' ({err})")))?;

    for entry in read_dir {
        let name = entry.file_name().to_string_lossy();
        if name == "." || name == ".." {
            continue;
        }
        let mut display = name.into_owned();
        append_directory_suffix(&mut display, entry.is_dir());
        entries.push(display);
    }

    sort_entries(&mut entries);
    Ok(entries)
}

async fn list_path(expanded: &str, original: &str) -> BuiltinResult<Vec<String>> {
    let path = PathBuf::from(expanded);
    match vfs::metadata_async(&path).await {
        Ok(metadata) => {
            if metadata.is_dir() {
                list_directory(&path).await
            } else {
                let mut text = path_to_string(&path);
                append_directory_suffix(&mut text, false);
                Ok(vec![text])
            }
        }
        Err(err) if err.kind() == ErrorKind::NotFound => Ok(Vec::new()),
        Err(err) => Err(ls_error(format!(
            "ls: unable to access '{original}' ({err})"
        ))),
    }
}

async fn list_glob_pattern(expanded: &str, original: &str) -> BuiltinResult<Vec<String>> {
    let mut entries = Vec::new();

    let matcher = glob(expanded)
        .map_err(|err| ls_error(format!("ls: invalid pattern '{original}' ({err})")))?;
    for item in matcher {
        match item {
            Ok(path) => {
                let is_dir = vfs::symlink_metadata_async(&path)
                    .await
                    .map(|meta| meta.is_dir())
                    .unwrap_or(false);
                let mut name = path_to_string(&path);
                append_directory_suffix(&mut name, is_dir);
                entries.push(name);
            }
            Err(err) => {
                return Err(ls_error(format!(
                    "ls: unable to enumerate matches for '{original}' ({err})"
                )))
            }
        }
    }

    Ok(entries)
}

fn rows_to_char_array(rows: &[String]) -> BuiltinResult<Value> {
    if rows.is_empty() {
        let array = CharArray::new(Vec::new(), 0, 0).map_err(|e| ls_error(format!("ls: {e}")))?;
        return Ok(Value::CharArray(array));
    }

    let width = rows
        .iter()
        .map(|row| row.chars().count())
        .max()
        .unwrap_or(0);

    let mut data = Vec::with_capacity(rows.len() * width);
    for row in rows {
        let mut chars: Vec<char> = row.chars().collect();
        while chars.len() < width {
            chars.push(' ');
        }
        data.extend(chars);
    }

    let array =
        CharArray::new(data, rows.len(), width).map_err(|e| ls_error(format!("ls: {e}")))?;
    Ok(Value::CharArray(array))
}

fn emit_listing_stdout(rows: &[String]) {
    if rows.is_empty() {
        return;
    }
    let text = rows.join("\n");
    record_console_line(ConsoleStream::Stdout, text);
}

fn should_emit_stdout() -> bool {
    match requested_output_count() {
        Some(0) => true,
        Some(_) => false,
        None => true,
    }
}

fn patterns_from_value(value: &Value) -> BuiltinResult<Vec<String>> {
    match value {
        Value::String(text) => Ok(vec![text.clone()]),
        Value::StringArray(StringArray { data, .. }) => {
            if data.len() == 1 {
                Ok(vec![data[0].clone()])
            } else {
                Err(ls_error(
                    "ls: name must be a character vector or string scalar",
                ))
            }
        }
        Value::CharArray(chars) => {
            if chars.rows != 1 {
                return Err(ls_error(
                    "ls: name must be a character vector or string scalar",
                ));
            }
            let mut row = String::with_capacity(chars.cols);
            for c in 0..chars.cols {
                row.push(chars.data[c]);
            }
            Ok(vec![row.trim_end().to_string()])
        }
        _ => Err(ls_error(
            "ls: name must be a character vector or string scalar",
        )),
    }
}

fn append_directory_suffix(text: &mut String, is_dir: bool) {
    if is_dir {
        let sep = std::path::MAIN_SEPARATOR;
        if !text.ends_with(sep) {
            text.push(sep);
        }
    }
}

async fn gather_arguments(args: &[Value]) -> BuiltinResult<Vec<Value>> {
    let mut out = Vec::with_capacity(args.len());
    for value in args {
        out.push(
            gather_if_needed_async(value)
                .await
                .map_err(map_control_flow)?,
        );
    }
    Ok(out)
}

#[cfg(test)]
pub(crate) mod tests {
    use super::super::REPL_FS_TEST_LOCK;
    use super::*;
    use runmat_builtins::CharArray;
    use runmat_filesystem::File;
    use tempfile::tempdir;

    fn ls_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
        futures::executor::block_on(super::ls_builtin(args))
    }

    struct DirGuard {
        original: PathBuf,
    }

    impl DirGuard {
        fn new() -> Self {
            let original = env::current_dir().expect("current dir");
            Self { original }
        }
    }

    impl Drop for DirGuard {
        fn drop(&mut self) {
            let _ = env::set_current_dir(&self.original);
        }
    }

    fn rows_from_value(value: Value) -> Vec<String> {
        match value {
            Value::CharArray(CharArray { data, rows, cols }) => {
                let mut out = Vec::with_capacity(rows);
                for r in 0..rows {
                    let mut row = String::with_capacity(cols);
                    for c in 0..cols {
                        row.push(data[r * cols + c]);
                    }
                    out.push(row.trim_end().to_string());
                }
                out
            }
            other => panic!("expected CharArray result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn ls_lists_current_directory_when_no_arguments() {
        let _lock = REPL_FS_TEST_LOCK
            .lock()
            .unwrap_or_else(|poison| poison.into_inner());
        let guard = DirGuard::new();

        let dir = tempdir().expect("tempdir");
        env::set_current_dir(dir.path()).expect("switch temp dir");
        File::create(dir.path().join("alpha.txt")).expect("create file");
        futures::executor::block_on(vfs::create_dir_async(dir.path().join("beta")))
            .expect("create dir");

        let value = ls_builtin(Vec::new()).expect("ls");
        let mut rows = rows_from_value(value);
        rows.sort();

        let sep = std::path::MAIN_SEPARATOR.to_string();
        assert_eq!(rows.len(), 2);
        assert!(rows.contains(&"alpha.txt".to_string()));
        assert!(rows.contains(&format!("beta{sep}")));

        drop(guard);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn ls_lists_specific_directory_contents() {
        let dir = tempdir().expect("tempdir");
        File::create(dir.path().join("data.csv")).expect("create file");
        futures::executor::block_on(vfs::create_dir_async(dir.path().join("nested")))
            .expect("create dir");

        let path = dir.path().to_string_lossy().to_string();
        let value = ls_builtin(vec![Value::from(path)]).expect("ls");
        let mut rows = rows_from_value(value);
        rows.sort();

        let sep = std::path::MAIN_SEPARATOR.to_string();
        assert_eq!(rows, vec!["data.csv".to_string(), format!("nested{sep}")]);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn ls_handles_wildcard_patterns() {
        let _lock = REPL_FS_TEST_LOCK
            .lock()
            .unwrap_or_else(|poison| poison.into_inner());
        let guard = DirGuard::new();

        let dir = tempdir().expect("tempdir");
        env::set_current_dir(dir.path()).expect("switch temp dir");

        File::create("alpha.m").expect("alpha");
        File::create("beta.txt").expect("beta");
        File::create("gamma.m").expect("gamma");

        let value = ls_builtin(vec![Value::from("*.m")]).expect("ls pattern");
        let mut rows = rows_from_value(value);
        rows.sort();

        assert_eq!(rows, vec!["alpha.m".to_string(), "gamma.m".to_string()]);

        drop(guard);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn ls_returns_path_for_single_file() {
        let dir = tempdir().expect("tempdir");
        let file_path = dir.path().join("report.md");
        File::create(&file_path).expect("create file");

        let value = ls_builtin(vec![Value::from(file_path.to_string_lossy().to_string())])
            .expect("ls file");
        let rows = rows_from_value(value);
        assert_eq!(rows, vec![file_path.to_string_lossy().to_string()]);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn ls_returns_empty_for_missing_matches() {
        let dir = tempdir().expect("tempdir");
        let path = dir.path().join("*.nomatch");
        let value =
            ls_builtin(vec![Value::from(path.to_string_lossy().to_string())]).expect("ls missing");
        match value {
            Value::CharArray(array) => {
                assert_eq!(array.rows, 0);
                assert_eq!(array.cols, 0);
                assert!(array.data.is_empty());
            }
            other => panic!("expected CharArray result, got {other:?}"),
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn ls_accepts_string_scalar_input() {
        let dir = tempdir().expect("tempdir");
        File::create(dir.path().join("file.dat")).expect("create file");

        let array = StringArray::new_2d(vec![dir.path().to_string_lossy().to_string()], 1, 1)
            .expect("string array");
        let value = ls_builtin(vec![Value::StringArray(array)]).expect("ls string");
        let rows = rows_from_value(value);
        assert!(rows.iter().any(|row| row.contains("file.dat")));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn ls_rejects_numeric_argument() {
        let err = ls_builtin(vec![Value::Num(1.0)]).expect_err("expected error");
        assert_eq!(
            err.message(),
            "ls: name must be a character vector or string scalar"
        );
    }
}
