use std::{fs, ops::Not, path::PathBuf, time::Duration};

use cicero_core::error::Context as _;
use tracing::{debug, trace};
use walkdir::WalkDir;

use super::{CacheId, ChangeIndicator};


pub(crate) trait Input {
    /// Unique and stable identifier for this `Input`.
    /// In the case of a file input, this is the file path, for example.
    fn cache_id(&self) -> CacheId;

    /// Value used to determine whether a change has happened to this `Input`.
    /// This value should change when a change to the input happened.
    /// In the case of a file input, this is the `last_modified` timestamp, for example.
    ///
    /// Returning [`ChangeIndicator::ExistsNot`] means the input stopped existing. It will be removed from the cache index
    /// and the next change (which doesn't result in a `ChangeIndicator::ExistsNot` change indicator) will be treated like the input was created fresh.
    fn change_indicator(&self) -> cicero_core::Result<ChangeIndicator>;
}

impl Input for PathBuf {
    fn cache_id(&self) -> CacheId {
        CacheId { value: self.to_string_lossy().into_owned() }
    }

    fn change_indicator(&self) -> cicero_core::Result<ChangeIndicator> {
        if self.exists() {
            let metadata = self.metadata()
                .context("Failed to read metadata of file {self:?} while checking for changes.")?;

            let latest_last_modified =
                if metadata.is_dir().not() {
                    last_modified(metadata)?
                } else {
                    let last_modified_timestamps = WalkDir::new(self) //includes the directory itself, which's last-modified is updated when a file/subdir gets added/renamed/removed
                        .follow_links(true)
                        .into_iter()
                        .filter_map(|entry|
                            entry
                                .inspect_err(|cause|
                                    debug!("Skipping a subdir or -file while checking for latest last_modified timestamp of input directory {}. A later timestamp might exist, which could lead to unnecessary rebuilds. Cannot check this subdir or -file, because an error occurred while walking the directory: {cause}", self.cache_id())
                                )
                                .ok()
                        )
                        .map(|entry| {
                            let metadata = entry.metadata()?;
                            let last_modified = last_modified(metadata)?;
                            Ok(last_modified)
                        })
                        .collect::<cicero_core::Result<Vec<_>>>()?;

                    last_modified_timestamps.into_iter().max()
                        .context("Did not find a latest timestamp")?
                };

            let change_indicator = latest_last_modified.as_nanos().to_string();
            Ok(ChangeIndicator::Exists(change_indicator))
        } else {
            Ok(ChangeIndicator::ExistsNot)
        }
    }
}

fn last_modified(metadata: fs::Metadata) -> cicero_core::Result<Duration> {
    let last_modified = metadata.modified()
        .context("Could not read last-modified time of file {self:?} while checking for changes.")?
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_else(|cause| {
            trace!("System clock may have gone backwards. Cache change indicator of file might indicate no change erronously, but continuing anyways.");
            cause.duration()
        });
    Ok(last_modified)
}


#[cfg(test)]
mod tests {
    use super::*;
    use assert_fs::{fixture::ChildPath, prelude::*};

    #[test]
    fn should_detect_a_change_for_a_file() -> cicero_core::Result<()> {
        let temp = assert_fs::TempDir::new()?;

        let testee = temp.child("testee.txt");

        assert_eq!(testee.to_path_buf().change_indicator()?, ChangeIndicator::ExistsNot);

        testee.write_str("created\n")?;
        let ChangeIndicator::Exists(created_timestamp) = testee.to_path_buf().change_indicator()?
        else { panic!("File should exist after creating it.") };

        testee.write_str("updated\n")?;
        let ChangeIndicator::Exists(updated_timestamp) = testee.to_path_buf().change_indicator()?
        else { panic!("File should exist after updating it.") };

        assert_ne!(created_timestamp, updated_timestamp);

        fs::remove_file(&testee)?;
        let ChangeIndicator::ExistsNot = testee.to_path_buf().change_indicator()?
        else { panic!("File should not exist after deleting it.") };

        Ok(())
    }

    #[test]
    fn should_detect_a_change_in_a_directory() -> cicero_core::Result<()> {
        let temp = assert_fs::TempDir::new()?;

        let testee = temp.child("testee/");

        assert_eq!(testee.to_path_buf().change_indicator()?, ChangeIndicator::ExistsNot);

        {
            testee.create_dir_all()?;

            let ChangeIndicator::Exists(change_indicator) = testee.to_path_buf().change_indicator()?
            else { panic!("Directory should be detected as existing after creating it.") };

            assert_eq!(change_indicator, testee.last_modified()?);
        }


        let child_file1 = testee.child("file1.txt");

        {
            child_file1.write_str("created\n")?;

            let ChangeIndicator::Exists(change_indicator) = testee.to_path_buf().change_indicator()?
            else { panic!("Directory should be detected as existing after creating file 1 in it.") };

            assert_eq!(change_indicator, child_file1.last_modified()?);
        }

        {
            child_file1.write_str("updated\n")?;

            let ChangeIndicator::Exists(change_indicator) = testee.to_path_buf().change_indicator()?
            else { panic!("Directory should be detected as existing after updating file 1 in it.") };

            assert_eq!(change_indicator, child_file1.last_modified()?);
        }

        let child_file2 = testee.child("file2.txt");

        {
            child_file2.write_str("created\n")?;

            let ChangeIndicator::Exists(change_indicator) = testee.to_path_buf().change_indicator()?
            else { panic!("Directory should be detected as existing after creating file 2 in it.") };

            assert_eq!(change_indicator, child_file2.last_modified()?);
        }


        {
            fs::remove_file(&child_file1)?;
            let ChangeIndicator::Exists(change_indicator) = testee.to_path_buf().change_indicator()?
            else { panic!("Directory should be detected as existing after deleting file 1 in it.") };

            assert_eq!(change_indicator, testee.last_modified()?); //the directory's last-modified updates when a file/subdir is added/renamed/removed
        }

        Ok(())
    }

    trait LastModified {
        fn last_modified(&self) -> cicero_core::Result<String>;
    }
    impl LastModified for ChildPath {
        fn last_modified(&self) -> cicero_core::Result<String> {
            let last_modified = fs::metadata(self.path())?
                .modified()?
                .duration_since(std::time::UNIX_EPOCH)?;

            Ok(last_modified.as_nanos().to_string())
        }
    }
}
