use std::{collections::HashMap, path::{Path, PathBuf}, sync::LazyLock};

use tracing::{debug, trace, warn};

use crate::{error::{InputCheckError, OutputCheckError}, index};

use super::{error::{CacheError, CacheResult}, index::{CacheIndex, IndexChangeIndicator, IndexOutputEntry}, input::Input, CacheId, ChangeIndicator};


/// Allows defining a cached file output and when it should be rebuilt.
///
/// Example usage:
/// ```no_run
/// use std::{fs, path::PathBuf};
///
/// let output_path = PathBuf::from("my/build/result.txt");
///
/// cicero_cache::Output::from(output_path.clone()).rebuild_on_change([
///     PathBuf::from("my/build/input.csv"),
/// ], || {
///     if output_path.exists() {
///         fs::remove_file(&output_path)?;
///     }
///     fs::create_dir_all(output_path.parent().unwrap())?;
///     fs::File::create(output_path)?;
///     Ok(())
/// });
/// ```
pub struct Output {
    pub path: PathBuf,
}

impl Output {
    pub fn rebuild_on_change(
        &self,
        inputs: impl AsRef<[PathBuf]>,
        code: impl FnOnce() -> UserResult,
    ) -> UserResult {
        let cache_index = &index::CACHE_INDEX;

        let ExplicitUserResult(result) = self.rebuild_on_change_with_cache_index(inputs, cache_index, code);
        result
    }


    fn rebuild_on_change_with_cache_index(
        &self,
        inputs: impl AsRef<[PathBuf]>,
        cache_index: &CacheIndex,
        code: impl FnOnce() -> UserResult,
    ) -> ExplicitUserResult {
        let inputs = {
            let mut inputs = inputs.as_ref().to_owned();
            inputs.push(BUILD_SCRIPT_CACHE_INVALIDATION_INDICATOR.to_path_buf());
            inputs
        };

        let output_id = self.path.cache_id();

        let needs_rebuild_result = self.determine_needs_rebuild_with_index_params(&output_id, inputs.as_ref(), cache_index);

        match needs_rebuild_result {
            Ok(needs_rebuild) => if let Some(write_updated_index_to_file_params) = needs_rebuild {
                self.do_rebuild(code, cache_index, Some(write_updated_index_to_file_params))
            } else {
                debug!("Skipping creation code for {path:?}. No changes happened to the inputs or the output since last execution.", path=self.path);
                ExplicitUserResult(Ok(()))
            }
            Err(cause) => {
                debug!("Rebuilding {output_id} unconditionally, because a failure occurred while checking whether it needs rebuilding: {cause}");
                let write_updated_index_to_file_params = None;
                self.do_rebuild(code, cache_index, write_updated_index_to_file_params)
            }
        }
    }

    fn calculate_input_change_indicators(inputs: &[PathBuf]) -> CacheResult<HashMap<CacheId, ChangeIndicator>> {
        inputs.iter()
            .map(|input| {
                let input_id = input.cache_id();
                let change_indicator = input.change_indicator()
                    .map_err(|source| InputCheckError::ChangeIndicator { input_id: input_id.clone(), source })?;

                Ok::<_, CacheError>((input_id, change_indicator))
            }).collect::<CacheResult<HashMap<_, _>>>()
    }

    fn determine_needs_rebuild_with_index_params(
        &self,
        output_id: &CacheId,
        inputs: &[PathBuf],
        cache_index: &CacheIndex,
    ) -> CacheResult<Option<WriteUpdatedIndexToFileParams>> {

        let previous_index_output_entry = cache_index.get(output_id);

        let input_change_indicators = Self::calculate_input_change_indicators(inputs)?;

        let needs_rebuild = self.determine_needs_rebuild(output_id, &input_change_indicators, previous_index_output_entry.as_ref())?;

        let params = needs_rebuild.then_some(
            WriteUpdatedIndexToFileParams {
                output_id: output_id.clone(),
                input_change_indicators,
            }
        );
        Ok(params)
    }

    fn determine_needs_rebuild(
        &self,
        output_id: &CacheId,
        input_change_indicators: &HashMap<CacheId, ChangeIndicator>,
        previous_index_output_entry: Option<&IndexOutputEntry>,
    ) -> CacheResult<bool> {
        if let Some(previous_index_output_entry) = previous_index_output_entry {

            let index::IndexOutputEntry {
                change_indicator: output_change_indicator,
                input_states_in_last_successful_build,
            } = previous_index_output_entry;

            let previous_inputs_changed = input_change_indicators.iter()
                .any(|(input_id, input_change_indicator)| {
                    let previous_change_indicator: &ChangeIndicator = &input_states_in_last_successful_build.get(input_id).into();

                    let changed = input_change_indicator.has_changed_from(previous_change_indicator);
                    if changed {
                        trace!("Detected change in input '{input_id}'. Rebuilding cache without checking other inputs.");
                    }
                    changed
                });

            let previous_output_changed = {
                let new_change_indicator = self.path.change_indicator()
                    .map_err(|source| OutputCheckError::PreviousChanged { output_id: output_id.clone(), source })?;

                let previous_change_indicator: ChangeIndicator = output_change_indicator.clone().into();

                let changed = new_change_indicator.has_changed_from(&previous_change_indicator);
                if changed {
                    trace!("Detected change in output '{output_id}'. Rebuilding cache.");
                }
                changed
            };

            Ok(previous_inputs_changed || previous_output_changed)
        } else {
            Ok(true)
        }
    }

    fn do_rebuild(
        &self,
        code: impl FnOnce() -> UserResult,
        cache_index: &CacheIndex,
        write_updated_index_to_file: Option<WriteUpdatedIndexToFileParams>,
    ) -> ExplicitUserResult {
        debug!("Executing provided creation code for {:?}.", self.path);

        let user_result = ExplicitUserResult(code());

        match &user_result.0 {
            Ok(()) => {
                debug!("Finished executing provided creation code for {:?}", self.path);

                if let Some(write_updated_index_to_file_params) = write_updated_index_to_file {
                    let _ignore_error =
                        self.write_updated_index_to_file(cache_index, write_updated_index_to_file_params)
                            .inspect_err(|cause| debug!("Failed to write cache index to file: {cause}"));
                } else {
                    debug!("Not writing cache index to file, because an error occurred when loading the index.");
                }
            }
            Err(cause) => {
                warn!("Error while executing user-provided creation code for {:?}: {cause}", self.path);
                debug!("Not writing cache index to file, because an error occurred in user-provided creation code for {:?}.", self.path);
            }
        }
        user_result
    }

    fn write_updated_index_to_file(
        &self,
        cache_index: &CacheIndex,
        params: WriteUpdatedIndexToFileParams,
    ) -> CacheResult<()> {
        let WriteUpdatedIndexToFileParams { output_id, input_change_indicators } = params;

        let new_output_change_indicator = self.path.change_indicator()
            .map_err(|source| OutputCheckError::ChangedAfterCreationCode { output_id: output_id.clone(), source })?;

        if let ChangeIndicator::Exists(new_output_change_indicator) = new_output_change_indicator {
            let new_input_items = input_change_indicators.iter()
                .filter_map(|(id, indicator)|
                    Option::<IndexChangeIndicator>::from(indicator.clone())
                        .map(|indicator| (id.clone(), indicator))
                )
                .collect();

            tracing::trace!("New input index items to store for output '{output_id}': {new_input_items:?}");

            let new_output_index = index::IndexOutputEntry {
                change_indicator: IndexChangeIndicator { value: new_output_change_indicator },
                input_states_in_last_successful_build: new_input_items,
            };

            cache_index.insert(output_id, new_output_index);

            cache_index.save_to_file()?;
        } else {
            debug!("Cached output {:?} does not exist after creation code execution. Not storing cache entries.", self.path);
        }

        Ok(())
    }
}
struct WriteUpdatedIndexToFileParams {
    output_id: CacheId,
    input_change_indicators: HashMap<CacheId, ChangeIndicator>,
}


impl From<PathBuf> for Output {
    fn from(path: PathBuf) -> Self {
        Self { path }
    }
}
impl From<&Path> for Output {
    fn from(path: &Path) -> Self {
        Self { path: path.to_path_buf() }
    }
}

type UserResult = anyhow::Result<()>; //We don't allow the user to pass out more than a failure for now, because we don't always run their code, so could at best provide an Option with their intended return value inside.

///Wrapper around UserResult to avoid bubbling all our errors into the anyhow::Result via `?`
#[must_use]
struct ExplicitUserResult(UserResult);

pub(crate) static BUILD_SCRIPT_CACHE_INVALIDATION_INDICATOR: LazyLock<PathBuf> = LazyLock::new(|| {
    //Watch build script executable for changes, so that we re-build when the build script changes.
    std::env::current_exe()
        .expect("Should be able to get path to build script executable for invalidating cache when the code changes")
});


#[cfg(test)]
mod tests {
    use std::ops::Not;
    use fs_err as fs;

    use assert_fs::{fixture::ChildPath, prelude::*};

    use super::*;

    #[test]
    fn should_execute_when_a_previous_input_is_uncached() -> anyhow::Result<()> {
        let Fixture { testee, input, mut cache_index, .. } = Fixture::create()?;

        input.a.touch()?;

        {
            let mut code_executed = false;

            let ExplicitUserResult(_) = testee.rebuild_on_change_with_cache_index(input.list, &mut cache_index, || {
                code_executed = true;
                Ok(())
            });
            assert!(code_executed);
        }

        Ok(())
    }

    /// Users may want to list optional inputs, which are taken into account, if they exist, but don't have to exist.
    #[test]
    fn should_execute_when_current_input_does_not_exist() -> anyhow::Result<()> {
        let Fixture { testee, input, mut cache_index, .. } = Fixture::create()?;

        assert!(input.a.exists().not());

        {
            let mut code_executed = false;

            let ExplicitUserResult(_) = testee.rebuild_on_change_with_cache_index(input.list, &mut cache_index, || {
                code_executed = true;
                Ok(())
            });
            assert!(code_executed);
        }

        Ok(())
    }

    #[test]
    fn should_not_execute_code_if_inputs_unchanged() -> anyhow::Result<()> {
        let Fixture { testee, input, mut cache_index, .. } = Fixture::create()?;

        input.a.write_str("Input a")?;

        {
            let mut code_executed = false;

            let ExplicitUserResult(_) = testee.rebuild_on_change_with_cache_index(&input.list, &mut cache_index, || {
                code_executed = true;
                fs::write(&testee.path, "Original output")?;
                Ok(())
            });
            assert!(code_executed);
        }

        {
            let mut code_executed = false;

            let ExplicitUserResult(_) = testee.rebuild_on_change_with_cache_index(&input.list, &mut cache_index, || {
                code_executed = true;
                Ok(())
            });
            assert!(code_executed.not());
        }

        Ok(())
    }

    #[test]
    fn should_execute_when_output_changed() -> anyhow::Result<()> {
        let Fixture { testee, input, mut cache_index, .. } = Fixture::create()?;

        input.a.touch()?;

        {
            let mut code_executed = false;

            let ExplicitUserResult(_) = testee.rebuild_on_change_with_cache_index(&input.list, &mut cache_index, || {
                code_executed = true;
                fs::write(&testee.path, "Original output")?;
                Ok(())
            });
            assert!(code_executed);
        }

        fs::write(&testee.path, "Changed output")?;

        {
            let mut code_executed = false;

            let ExplicitUserResult(_) = testee.rebuild_on_change_with_cache_index(&input.list, &mut cache_index, || {
                code_executed = true;
                Ok(())
            });
            assert!(code_executed);
        }

        fs::remove_file(input.a)?;

        {
            let mut code_executed = false;

            let ExplicitUserResult(_) = testee.rebuild_on_change_with_cache_index(&input.list, &mut cache_index, || {
                code_executed = true;
                Ok(())
            });
            assert!(code_executed);
        }

        Ok(())
    }

    struct Fixture {
        testee: Output,
        input: InputFixture,
        cache_index: CacheIndex,
        _temp: assert_fs::TempDir, //carried along to prevent drop before test ends
    }
    struct InputFixture {
        list: Vec<PathBuf>,
        a: ChildPath,
    }
    impl Fixture {
        fn create() -> anyhow::Result<Self> {
            let temp = assert_fs::TempDir::new()?;

            let output_path = temp.child("output.txt");
            let testee = Output::from(output_path.to_path_buf());

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

            let inputs = vec![
                input_a.to_path_buf(),
            ];

            let cache_index_file = temp.child("cache_index.json");
            let cache_index = CacheIndex::load_from_file(&cache_index_file)
                .map_err(CacheError::from)?;

            Ok(Self {
                testee,
                input: InputFixture {
                    list: inputs,
                    a: input_a,
                },
                cache_index,
                _temp: temp,
            })
        }
    }
}
