use crate::interop::FfResult;
use crate::prelude::{err_str_tuple, object, ok_tuple};
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Bytes, Read, Write};
use std::path::Path;
use std::sync::{Arc, Mutex};

use im::Vector;
use serde::Serialize;

use crate::interpreter::{LazyState, RuntimeError, RuntimeResult, Value, type_name};
use crate::{members, native};

members! {
    "Fs",
    open => native!(1, move |_env, args| {
        let Value::String(path) = &args[0] else {
            return Err(RuntimeError::NativeTypeError {
                native: "Fs.open",
                expected: "string",
                got: type_name(&args[0]),
            });
        };
        if !Path::new(&**path).exists() {
            err_str_tuple("File not exists");
        }
        Ok(file_object(path.clone()))
    }),
    exists => |path: String| -> FfResult<bool> {
        fs::exists(path).into()
    },
    read_dir => native!(1, move |_env, args| {
        let Value::String(path) = &args[0] else {
            return Err(RuntimeError::NativeTypeError {
                native: "Fs.read_dir",
                expected: "string",
                got: type_name(&args[0]),
            });
        };
        Ok(match fs::read_dir(&**path) {
            Ok(iter) => ok_tuple(dir_stream(iter)?),
            Err(e) => err_str_tuple(e.to_string()),
        })
    }),
}

fn dir_stream(mut iter: fs::ReadDir) -> RuntimeResult<Value> {
    match iter.next() {
        Some(res) => match res {
            Ok(entry) => {
                let tail = Arc::new(Mutex::new(LazyState::Native(Box::new(move || {
                    dir_stream(iter)
                }))));
                Ok(Value::Cons {
                    head: Arc::new(ok_tuple(dir_entry_object(entry))),
                    tail,
                })
            }
            Err(e) => Ok(err_str_tuple(e.to_string())),
        },
        None => Ok(Value::List(Vector::new())),
    }
}

fn dir_entry_object(entry: fs::DirEntry) -> Value {
    let path: Arc<str> = entry.path().to_string_lossy().into();
    let name: Arc<str> = entry.file_name().to_string_lossy().into();
    let (is_file, is_dir, is_symlink) = match entry.file_type() {
        Ok(ft) => (ft.is_file(), ft.is_dir(), ft.is_symlink()),
        Err(_) => (false, false, false),
    };
    object(vec![
        ("name", Value::String(name)),
        ("path", Value::String(path)),
        ("is_file", Value::Bool(is_file)),
        ("is_dir", Value::Bool(is_dir)),
        ("is_symlink", Value::Bool(is_symlink)),
    ])
}

// A file handle object: a few static fields (path) plus a fixed set of
// callable methods. The methods can't be a `members!` block because they
// all close over `path` — `members!` produces a single `fn members()` and
// can't capture per-instance state.
fn file_object(path: Arc<str>) -> Value {
    let entries = vec![
        ("path", Value::String(path.clone())),
        ("write", write_fn(path.clone())),
        ("append", append_fn(path.clone())),
        ("read_all", read_all(path.clone())),
        ("lines", lines_fn(path.clone())),
        ("bytes", bytes_fn(path.clone())),
        ("metadata", metadata_fn(path)),
    ];
    object(entries)
}

fn write_fn(path: Arc<str>) -> Value {
    crate::interop::native_fn("file.write", move |data: String| -> FfResult<()> {
        std::fs::write(&*path, &data).into()
    })
}

fn append_fn(path: Arc<str>) -> Value {
    crate::interop::native_fn("file.append", move |data: String| -> FfResult<()> {
        std::fs::OpenOptions::new()
            .append(true)
            .open(&*path)
            .and_then(|mut f| f.write_all(data.as_bytes()))
            .into()
    })
}

fn read_all(path: Arc<str>) -> Value {
    crate::interop::native_fn("file.read_all", move || -> FfResult<String> {
        std::fs::read_to_string(&*path).into()
    })
}

fn bytes_fn(path: Arc<str>) -> Value {
    native!(0, move |_env, _args| {
        Ok(match File::open(&*path) {
            Ok(f) => {
                let reader = BufReader::new(f);
                ok_tuple(bytes_stream(reader.bytes())?)
            }
            Err(e) => err_str_tuple(e.to_string()),
        })
    })
}

fn bytes_stream(mut bytes: Bytes<BufReader<File>>) -> RuntimeResult<Value> {
    match bytes.next() {
        Some(res) => match res {
            Ok(p) => {
                let tail = Arc::new(Mutex::new(LazyState::Native(Box::new(move || {
                    bytes_stream(bytes)
                }))));
                Ok(Value::Cons {
                    head: Arc::new(ok_tuple(Value::Number(Arc::new(p.into())))),
                    tail,
                })
            }
            Err(e) => Ok(err_str_tuple(e.to_string())),
        },
        None => Ok(Value::List(Vector::new())),
    }
}

#[derive(Serialize)]
struct FileMetadata {
    size: u64,
    is_file: bool,
    is_dir: bool,
}

fn metadata_fn(path: Arc<str>) -> Value {
    crate::interop::native_fn("file.metadata", move || -> FfResult<FileMetadata> {
        fs::metadata(&*path)
            .map(|m| FileMetadata {
                size: m.len(),
                is_file: m.is_file(),
                is_dir: m.is_dir(),
            })
            .into()
    })
}

fn lines_fn(path: Arc<str>) -> Value {
    native!(0, move |_env, _args| {
        Ok(match File::open(&*path) {
            Ok(f) => ok_tuple(lines_stream(BufReader::new(f))?),
            Err(e) => err_str_tuple(e.to_string()),
        })
    })
}

// Walk a `BufReader` one line at a time, producing a `Cons` spine whose tail
// is a native thunk capturing the (advanced) reader. EOF terminates the spine
// with `Value::List(empty)` so consumers that flatten or pattern-match see a
// proper sequence terminator.
fn lines_stream(mut reader: BufReader<File>) -> RuntimeResult<Value> {
    let mut buf = String::new();
    match reader.read_line(&mut buf) {
        Ok(0) => Ok(Value::List(Vector::new())),
        Ok(_) => {
            strip_line_ending(&mut buf);
            let tail = Arc::new(Mutex::new(LazyState::Native(Box::new(move || {
                lines_stream(reader)
            }))));
            Ok(Value::Cons {
                head: Arc::new(ok_tuple(Value::String(buf.into()))),
                tail,
            })
        }
        Err(e) => Ok(err_str_tuple(e.to_string())),
    }
}

fn strip_line_ending(s: &mut String) {
    if s.ends_with('\n') {
        s.pop();
        if s.ends_with('\r') {
            s.pop();
        }
    }
}
