use crate::discovery::try_read_hcl_file;
use hcl::edit::structure::Body;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use tokio::task::JoinSet;
use tracing::debug;

#[derive(Debug, Clone, Copy)]
pub enum DiscoveryDepth {
    Shallow,
    Recursive,
}

#[derive(Debug)]
enum ActionResponse {
    DiscoveredChildFile(PathBuf),
    DiscoverDirChildren(PathBuf),
    DiscoveredBody(PathBuf, Body),
    None,
}

pub async fn discover_hcl(
    directory: impl AsRef<Path>,
    depth: DiscoveryDepth,
) -> eyre::Result<HashMap<PathBuf, Body>> {
    let mut join_set: JoinSet<eyre::Result<ActionResponse>> = JoinSet::new();
    let dir = directory.as_ref().to_path_buf();
    join_set.spawn(async move { Ok(ActionResponse::DiscoverDirChildren(dir)) });
    let mut hcl_bodies = HashMap::new();
    while let Some(res) = join_set.join_next().await {
        let res = res??;
        debug!(?res, "Handling discovery action response");
        match res {
            ActionResponse::None => {}
            ActionResponse::DiscoveredChildFile(path) => {
                join_set.spawn(async move {
                    if let Some(body) = try_read_hcl_file(&path).await? {
                        Ok(ActionResponse::DiscoveredBody(path, body))
                    } else {
                        Ok(ActionResponse::None)
                    }
                });
            }
            ActionResponse::DiscoveredBody(path, body) => {
                hcl_bodies.insert(path, body);
            }
            ActionResponse::DiscoverDirChildren(dir) => {
                let mut entries = match tokio::fs::read_dir(&dir).await {
                    Ok(entries) => entries,
                    Err(_) => continue,
                };

                while let Some(entry) = entries.next_entry().await? {
                    let path = entry.path();
                    let metadata = entry.metadata().await?;

                    if metadata.is_file() {
                        join_set
                            .spawn(async move { Ok(ActionResponse::DiscoveredChildFile(path)) });
                    } else if metadata.is_dir() && matches!(depth, DiscoveryDepth::Recursive) {
                        let path_clone = path.clone();
                        join_set.spawn(async move {
                            Ok(ActionResponse::DiscoverDirChildren(path_clone))
                        });
                    }
                }
            }
        }
    }
    Ok(hcl_bodies)
}

#[cfg(test)]
mod test {
    use crate::discovery::DiscoveryDepth;
    use crate::discovery::discover_hcl;
    use std::path::PathBuf;
    use tracing::level_filters::LevelFilter;
    use tracing_subscriber::EnvFilter;

    fn init_logging() -> eyre::Result<()> {
        let env_filter = EnvFilter::builder()
            .with_default_directive(LevelFilter::DEBUG.into())
            .from_env_lossy();
        let subscriber = tracing_subscriber::fmt()
            .with_env_filter(env_filter)
            .with_file(true)
            .with_line_number(true)
            .without_time();
        if let Err(error) = subscriber.try_init() {
            eprintln!(
                "Failed to initialize tracing subscriber - are you running `cargo test`? If so, multiple test entrypoints may be running from the same process. https://github.com/tokio-rs/console/issues/505 : {error}"
            );
            return Ok(());
        }
        Ok(())
    }

    #[tokio::test]
    pub async fn it_works() -> eyre::Result<()> {
        init_logging()?;

        let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(PathBuf::from_iter([
            "..",
            "..",
            "test_data",
            "terraform_discovery",
        ]));
        assert!(dir.exists());

        let discovered_flat = discover_hcl(&dir, DiscoveryDepth::Shallow).await?;
        assert_eq!(discovered_flat.len(), 2);

        let discovered_recursive = discover_hcl(&dir, DiscoveryDepth::Recursive).await?;
        assert_eq!(discovered_recursive.len(), 6);
        Ok(())
    }
}
