use std::{
    env, fmt, fs,
    fs::File,
    io::{self, Read, Write},
    path::{Path, PathBuf},
    process,
    str::FromStr,
    sync::atomic,
    time::Duration,
};

use uuid::Uuid;

use crate::Csv;

static QSV_INTEGRATION_TEST_DIR: &str = "xit";

static NEXT_ID: atomic::AtomicUsize = atomic::AtomicUsize::new(0);

pub struct Workdir {
    root:     PathBuf,
    dir:      PathBuf,
    flexible: bool,
}

impl Drop for Workdir {
    fn drop(&mut self) {
        if let Err(err) = fs::remove_dir_all(&self.dir) {
            panic!("Could not remove '{:?}': {err}", self.dir);
        }
    }
}

impl Workdir {
    pub fn new(name: &str) -> Workdir {
        let id = NEXT_ID.fetch_add(1, atomic::Ordering::SeqCst);
        let mut root = env::current_exe()
            .unwrap()
            .parent()
            .expect("executable's directory")
            .to_path_buf();
        if root.ends_with("deps") {
            root.pop();
        }
        let dir = root
            .join(QSV_INTEGRATION_TEST_DIR)
            .join(name)
            .join(format!("test-{id}-{}", Uuid::new_v4()));
        if let Err(err) = create_dir_all(&dir) {
            panic!("Could not create '{dir:?}': {err}");
        }
        Workdir {
            root,
            dir,
            flexible: false,
        }
    }

    pub fn flexible(mut self, yes: bool) -> Workdir {
        self.flexible = yes;
        self
    }

    /// create a file with the default comma delimiter
    pub fn create<T: Csv>(&self, name: &str, rows: T) {
        self.create_with_delim(name, rows, b',')
    }

    /// create a file with the specified delimiter
    pub fn create_with_delim<T: Csv>(&self, name: &str, rows: T, delim: u8) {
        let mut wtr = csv::WriterBuilder::new()
            .flexible(self.flexible)
            .delimiter(delim)
            .from_path(self.path(name))
            .unwrap();
        for row in rows.to_vecs() {
            wtr.write_record(row).unwrap();
        }
        wtr.flush().unwrap();
        // Attempt "to sync all OS-internal file content and metadata to disk"
        let _ = fs::File::open(self.path(name)).unwrap().sync_all();
    }

    /// create a file and index it
    pub fn create_indexed<T: Csv>(&self, name: &str, rows: T) {
        self.create(name, rows);

        let mut cmd = self.command("index");
        cmd.arg(name);
        self.run(&mut cmd);
    }

    /// create a file with the specified string data
    pub fn create_from_string(&self, name: &str, data: &str) {
        let filename = &self.path(name);
        let mut file = File::create(filename).unwrap();
        file.write_all(data.as_bytes()).unwrap();
        file.flush().unwrap();
    }

    /// read the contents of a file into a string
    pub fn read_to_string(&self, filename: &str) -> io::Result<String> {
        let mut file = File::open(self.path(filename))?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        Ok(contents)
    }

    /// read stdout of a command
    pub fn read_stdout<T: Csv>(&self, cmd: &mut process::Command) -> T {
        let stdout: String = self.stdout(cmd);
        let mut rdr = csv::ReaderBuilder::new()
            .has_headers(false)
            .flexible(true)
            .from_reader(io::Cursor::new(stdout));

        let records: Vec<Vec<String>> = rdr
            .records()
            .collect::<Result<Vec<csv::StringRecord>, _>>()
            .unwrap()
            .into_iter()
            .map(|r| r.iter().map(std::string::ToString::to_string).collect())
            .collect();
        Csv::from_vecs(records)
    }

    /// Run `cmd`, assert it exited successfully, and return its stdout parsed
    /// as CSV. Use this instead of `assert_success` followed by `read_stdout`,
    /// which executes `cmd` twice — the second run could fail yet still emit
    /// parseable CSV, silently masking the failure.
    pub fn read_stdout_on_success<T: Csv>(&self, cmd: &mut process::Command) -> T {
        let o = cmd.output().unwrap();
        assert!(
            o.status.success(),
            "\n\n===== {:?} =====\ncommand failed but expected success!\n\ncwd: {}\n\nstatus: \
             {}\n\nstdout: {}\n\nstderr: {}\n\n=====\n",
            cmd,
            self.dir.display(),
            o.status,
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        );
        let mut rdr = csv::ReaderBuilder::new()
            .has_headers(false)
            .flexible(true)
            .from_reader(io::Cursor::new(o.stdout));

        let records: Vec<Vec<String>> = rdr
            .records()
            .collect::<Result<Vec<csv::StringRecord>, _>>()
            .unwrap()
            .into_iter()
            .map(|r| r.iter().map(std::string::ToString::to_string).collect())
            .collect();
        Csv::from_vecs(records)
    }

    pub fn command(&self, command_str: &str) -> process::Command {
        let mut cmd = process::Command::new(self.qsv_bin());
        if command_str.is_empty() {
            cmd.current_dir(&self.dir);
        } else {
            cmd.current_dir(&self.dir).arg(command_str);
        }
        cmd
    }

    pub fn output(&self, cmd: &mut process::Command) -> process::Output {
        cmd.output().unwrap()
    }

    pub fn run(&self, cmd: &mut process::Command) {
        self.output(cmd);
    }

    pub fn stdout<T: FromStr>(&self, cmd: &mut process::Command) -> T {
        let o = self.output(cmd);
        let stdout = String::from_utf8_lossy(&o.stdout);
        stdout
            .trim_matches(&['\r', '\n'][..])
            .parse()
            .ok()
            .unwrap_or_else(|| panic!("Could not convert from string: '{stdout}'"))
    }

    pub fn output_stderr(&self, cmd: &mut process::Command) -> String {
        {
            // ensures stderr has been flushed before we run our cmd
            let mut _stderr = io::stderr();
            _stderr.flush().unwrap();
        }
        let o = cmd.output().unwrap();
        let o_utf8 = String::from_utf8_lossy(&o.stderr).to_string();
        if !o.status.success() || !o_utf8.is_empty() {
            o_utf8
        } else {
            "No error".to_string()
        }
    }

    /// Run `cmd`, assert it exited successfully, and return its stderr as a
    /// `String`. Use this when a test needs to inspect stderr text (e.g. a
    /// `wwarn!` message) while also guaranteeing the command did not error
    /// out — `output_stderr` alone returns stderr even on non-zero exit and
    /// can mask command failures.
    pub fn stderr_on_success(&self, cmd: &mut process::Command) -> String {
        {
            // ensures stderr has been flushed before we run our cmd
            let mut _stderr = io::stderr();
            _stderr.flush().unwrap();
        }
        let o = cmd.output().unwrap();
        assert!(
            o.status.success(),
            "\n\n===== {:?} =====\ncommand failed but expected success!\n\ncwd: {}\n\nstatus: \
             {}\n\nstdout: {}\n\nstderr: {}\n\n=====\n",
            cmd,
            self.dir.display(),
            o.status,
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        );
        String::from_utf8_lossy(&o.stderr).to_string()
    }

    /// Run `cmd`, assert it exited successfully, and return its stdout parsed
    /// as `T`. Unlike calling `assert_success` then `stdout` (which executes the
    /// command twice and only asserts success on the first run), this captures a
    /// single `Output`, so the run whose success is asserted is the same run
    /// whose stdout is returned — a failing command cannot be masked.
    pub fn stdout_on_success<T: FromStr>(&self, cmd: &mut process::Command) -> T {
        let o = self.output(cmd);
        assert!(
            o.status.success(),
            "\n\n===== {:?} =====\ncommand failed but expected success!\n\ncwd: {}\n\nstatus: \
             {}\n\nstdout: {}\n\nstderr: {}\n\n=====\n",
            cmd,
            self.dir.display(),
            o.status,
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        );
        let stdout = String::from_utf8_lossy(&o.stdout);
        stdout
            .trim_matches(&['\r', '\n'][..])
            .parse()
            .ok()
            .unwrap_or_else(|| panic!("Could not convert from string: '{stdout}'"))
    }

    pub fn assert_success(&self, cmd: &mut process::Command) {
        let o = cmd.output().unwrap();
        assert!(
            o.status.success(),
            "\n\n===== {:?} =====\ncommand failed but expected success!\n\ncwd: {}\n\nstatus: \
             {}\n\nstdout: {}\n\nstderr: {}\n\n=====\n",
            cmd,
            self.dir.display(),
            o.status,
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        );
    }

    pub fn assert_err(&self, cmd: &mut process::Command) {
        let o = cmd.output().unwrap();
        assert!(
            !o.status.success(),
            "\n\n===== {:?} =====\ncommand succeeded but expected failure!\n\ncwd: {}\n\nstatus: \
             {}\n\nstdout: {}\n\nstderr: {}\n\n=====\n",
            cmd,
            self.dir.display(),
            o.status,
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        );
    }

    /// returns contents of specified file in resources/test directory
    pub fn load_test_resource(&self, filename: &str) -> String {
        // locate resources/test relative to crate base dir
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push("resources/test/");
        path.push(filename);

        self.from_str::<String>(path.as_path())
    }

    /// copy the file in resources/test directory to the working directory
    /// returns absolute file path of the copied file
    pub fn load_test_file(&self, filename: &str) -> String {
        // locate resources/test relative to crate base dir
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push("resources/test/");
        path.push(filename);

        let resource_file_path = path.into_os_string().into_string().unwrap();

        let mut wrkdir_path = self.dir.clone();
        wrkdir_path.push(filename);

        fs::copy(resource_file_path, wrkdir_path.clone()).unwrap();

        // Attempt "to sync all OS-internal file content and metadata to disk"
        let _ = fs::File::open(wrkdir_path.clone()).unwrap().sync_all();

        wrkdir_path.into_os_string().into_string().unwrap()
    }

    #[allow(clippy::wrong_self_convention)]
    /// returns the contents of the file
    pub fn from_str<T: FromStr>(&self, name: &Path) -> T {
        log::debug!("reading file: {name:?}");
        let mut o = String::new();
        fs::File::open(name)
            .unwrap()
            .read_to_string(&mut o)
            .unwrap();
        o.parse().ok().expect("fromstr")
    }

    /// returns the path to the given filename in the working directory
    pub fn path(&self, name: &str) -> PathBuf {
        self.dir.join(name)
    }

    #[cfg(feature = "qsvmcp")]
    pub fn qsv_bin(&self) -> PathBuf {
        self.root.join("qsvmcp")
    }

    #[cfg(all(feature = "feature_capable", not(feature = "qsvmcp")))]
    pub fn qsv_bin(&self) -> PathBuf {
        self.root.join("qsv")
    }

    #[cfg(feature = "lite")]
    pub fn qsv_bin(&self) -> PathBuf {
        self.root.join("qsvlite")
    }

    #[cfg(feature = "datapusher_plus")]
    pub fn qsv_bin(&self) -> PathBuf {
        self.root.join("qsvdp")
    }

    /// clear all files in directory
    pub fn clear_contents(&self) -> io::Result<()> {
        for entry in fs::read_dir(&self.dir)? {
            fs::remove_file(entry?.path())?;
        }
        Ok(())
    }

    // create a subdirectory
    pub fn create_subdir(&self, name: &str) -> io::Result<()> {
        let mut path = self.dir.clone();
        path.push(name);
        create_dir_all(path)
    }

    /// Read a CSV file and parse it into Vec<Vec<String>>
    /// Note that this does not return the header row
    pub fn read_csv(&self, name: &str) -> Vec<Vec<String>> {
        let path = self.path(name);
        let mut rdr = csv::ReaderBuilder::new()
            .flexible(self.flexible)
            .from_path(&path)
            .unwrap();
        rdr.records()
            .map(|r| r.unwrap().iter().map(|s| s.to_string()).collect())
            .collect()
    }
}

impl fmt::Debug for Workdir {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "path={}", self.dir.display())
    }
}

// For whatever reason, `fs::create_dir_all` fails intermittently on CI
// with a weird "file exists" error. Despite my best efforts to get to the
// bottom of it, I've decided a try-wait-and-retry hack is good enough.
fn create_dir_all<P: AsRef<Path>>(p: P) -> io::Result<()> {
    let mut last_err = None;
    for _ in 0..10 {
        match fs::create_dir_all(&p) {
            Err(err) => {
                last_err = Some(err);
                ::std::thread::sleep(Duration::from_millis(500));
            },
            _ => {
                return Ok(());
            },
        }
    }
    Err(last_err.unwrap())
}

#[cfg(all(feature = "to", feature = "feature_capable"))]
pub fn is_same_file(file1: &Path, file2: &Path) -> Result<bool, std::io::Error> {
    use std::io::BufReader;

    let f1 = File::open(file1)?;
    let f2 = File::open(file2)?;

    // Check if file sizes are different
    if f1.metadata().unwrap().len() != f2.metadata().unwrap().len() {
        return Ok(false);
    }

    // Use buf readers since they are much faster
    let f1 = BufReader::new(f1);
    let f2 = BufReader::new(f2);

    // Do a byte to byte comparison of the two files
    for (b1, b2) in f1.bytes().zip(f2.bytes()) {
        if b1.unwrap() != b2.unwrap() {
            return Ok(false);
        }
    }

    Ok(true)
}

/// Build a multi-GB CSV (10M rows × 5 columns of padded strings) for tests
/// that need to deterministically trigger `util::mem_file_check` OOM via
/// `QSV_FREEMEMORY_HEADROOM_PCT=90 --memcheck`. Returns the `Workdir` and the
/// absolute path to the created file. Shared so the row count, column shape,
/// and padding stay in sync between `test_stats.rs` and `test_frequency.rs`.
///
/// Rows are streamed directly to a `csv::Writer` on disk rather than
/// materialized into an in-memory `Vec<Vec<String>>` first — the latter would
/// retain ~1.5 GB of `String`s in the test process before any write hits the
/// disk, which is enough to OOM the test harness itself on memory-constrained
/// hosts (defeating the point of these OOM-fallback tests).
pub fn build_large_oom_csv(name: &str) -> (Workdir, std::path::PathBuf) {
    let wrk = Workdir::new(name);
    let path = wrk.path("large_data.csv");
    let mut wtr = csv::WriterBuilder::new().from_path(&path).unwrap();
    wtr.write_record(["col1", "col2", "col3", "col4", "col5"])
        .unwrap();
    for i in 0..10_000_000_u64 {
        wtr.write_record([
            format!("value_{i}_with_some_padding_to_make_it_larger"),
            format!("another_value_{i}_with_more_data"),
            format!("data_{i}"),
            format!("field_{i}_content"),
            format!("final_field_{i}_with_additional_text"),
        ])
        .unwrap();
    }
    wtr.flush().unwrap();
    drop(wtr);
    let _ = fs::File::open(&path).unwrap().sync_all();
    (wrk, path)
}
