bular 0.0.2

CLI for managing Bular deployments
use std::{
    collections::{HashMap, HashSet},
    path::PathBuf,
    process::Stdio,
};

use cargo_metadata::{CargoOpt, Metadata, MetadataCommand};
use rustdoc_types::{Crate, Item, ItemEnum, StructKind, Type};

use crate::manifest::{Entrypoint, EnvironmentVariable, Manifest, Resource};

static CLOUD_SDK_PKG_NAME: &str = "cloud-sdk";
static CLOUD_SDK_PKG_NAME_2: &str = "cloud_sdk";

// TODO: Put on struct so this is reusable and more flexible.
pub fn plan(cwd: &PathBuf) -> (Manifest, PathBuf) {
    let metadata = MetadataCommand::new()
        .features(CargoOpt::AllFeatures) // TODO: From CLI arguments
        .current_dir(cwd)
        .exec()
        .unwrap();

    // If a package depends on `cloud-sdk`, it is considered a tracked package. // TODO
    let tracked_packages = metadata
        .packages
        .iter()
        .filter(|p| {
            p.dependencies
                .iter()
                .find(|x| x.name == CLOUD_SDK_PKG_NAME)
                .is_some()
        })
        .collect::<Vec<_>>();

    let entrypoints = tracked_packages
        .iter()
        .map(|p| {
            p.targets
                .iter()
                // TODO: I wonder if libraries should be tracked to support resources defined in dependencies (Eg. a core crate)
                .filter(|t| t.kind.iter().find(|k| &k.to_string() == "bin").is_some())
                .map(|t| (p.name.clone(), t.name.clone()))
                .collect::<Vec<_>>()
        })
        .flatten()
        .collect::<Vec<_>>();

    let unique = entrypoints
        .iter()
        .map(|(_, bin)| bin)
        .collect::<HashSet<_>>();
    if unique.len() != entrypoints.len() {
        // exit status: 101 "error: document output filename collision\nThe bin `b` in package `specta-cloud-example-playground v0.0.0 (/Users/oscar/Desktop/start/examples/playground)` has the same name as the bin `b` in package `specta-cloud-example-all v0.0.0 (/Users/oscar/Desktop/start/examples/all)`.\nOnly one may be documented at once since they output to the same path.\nConsider documenting only one, renaming one, or marking one with `doc = false` in Cargo.toml.\n"
        panic!("Duplicate entrypoint found. This is not supported by Rustdoc.");
    }

    // TODO: Workout the SDK version and ensure the CLI handles changes.
    // TODO: Error out if it's too old.

    check_for_nightly_toolchain();

    println!("Generating Cargo workspace documentation...");
    let doc = std::process::Command::new("cargo")
        .args(&["+nightly", "doc", "-q", "--no-deps"])
        .args(tracked_packages.iter().map(|p| ["-p", &p.name]).flatten())
        .env(
            "RUSTDOCFLAGS",
            "--document-private-items -Zunstable-options -wjson --document-hidden-items",
        )
        .output()
        .unwrap();

    if !doc.status.success() {
        println!("{} {:?}", doc.status, String::from_utf8_lossy(&doc.stderr));
        panic!("Failed to generate documentation");
    }

    // TODO: This should probs be able to be modified???
    let name = metadata.workspace_root.file_name().unwrap().to_string();

    let mut manifest = Manifest {
        name,
        entrypoints: entrypoints
            .iter()
            .map(|(crate_name, binary_name)| {
                (
                    binary_name.clone(),
                    Entrypoint {
                        _crate: crate_name.clone(),
                        environment: Default::default(),
                        resources: Default::default(),
                        lambda: None,
                    },
                )
            })
            .collect::<HashMap<_, _>>(),
    };

    for (crate_name, binary_name) in entrypoints {
        let docs = load_rust_docs(&metadata, &binary_name);
        State {
            docs: &docs,
            crate_name: crate_name.clone(),
            binary_name: binary_name.clone(),
            manifest: &mut manifest,
        }
        .parse_crate_docs();
    }

    (manifest, metadata.target_directory.into())
}

fn load_rust_docs(metadata: &Metadata, binary_name: &str) -> Crate {
    let path = metadata
        .target_directory
        .join("doc")
        .join(&format!("{}.json", normalize_crate_name(&binary_name)));
    let content = std::fs::read_to_string(&path).unwrap();
    serde_json::from_str(&content).unwrap()
}

#[derive(Default)]
struct Resources {
    resources: HashSet<String>,
    env: HashSet<String>,
}

fn check_for_nightly_toolchain() {
    if !std::process::Command::new("cargo")
        .args(&["+nightly", "version"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .unwrap()
        .success()
    {
        panic!("Nightly toolchain not found. Please install it using `rustup toolchain install nightly`.");
    }
}

struct State<'a> {
    docs: &'a Crate,
    crate_name: String,
    binary_name: String,
    manifest: &'a mut Manifest,
}

impl<'a> State<'a> {
    fn parse_crate_docs(&mut self) {
        for (id, item) in self.docs.index.iter() {
            match &item.inner {
                // ItemEnum::Module(module) => todo!(),
                // ItemEnum::ExternCrate { name, rename } => todo!(),
                // ItemEnum::Use(_) => todo!(),
                // ItemEnum::Union(union) => todo!(),
                ItemEnum::Struct(s) => {
                    // println!("Found struct {:?}", item.name);
                    // println!("{:?}\n{:?}\n", item, s);

                    match &s.kind {
                        StructKind::Unit => {}
                        StructKind::Tuple(_) => {}
                        StructKind::Plain {
                            fields,
                            has_stripped_fields,
                        } => {
                            for field in fields {
                                let got = self.docs.index.get(field).unwrap();
                                // println!("\t{:?} {:?}", field, got);
                            }
                        }
                    }
                }
                ItemEnum::StructField(f) => {
                    match &f {
                        Type::ResolvedPath(path) => {
                            let path2 = self.docs.paths.get(&path.id).unwrap();
                            let crate_info =
                                self.docs.external_crates.get(&path2.crate_id).unwrap();

                            if crate_info.name == CLOUD_SDK_PKG_NAME_2 {
                                // TODO: This is horid
                                let identifier = path2.path.get(1).unwrap();

                                // TODO: This should be opt-in not opt-out.
                                if identifier == "Sealed" || identifier == "sealed" {
                                    continue;
                                }

                                self.manifest
                                    .entrypoints
                                    .get_mut(&self.binary_name)
                                    .unwrap()
                                    .resources
                                    .insert(match &**identifier {
                                        "Database" => Resource::Database,
                                        _ => todo!("invalid identifier {identifier:?}"),
                                    });
                            }
                        }
                        //     Type::DynTrait(dyn_trait) => todo!(),
                        //     Type::Generic(_) => todo!(),
                        //     Type::Primitive(_) => todo!(),
                        //     Type::FunctionPointer(function_pointer) => todo!(),
                        //     Type::Tuple(vec) => todo!(),
                        //     Type::Slice(_) => todo!(),
                        //     Type::Array { type_, len } => todo!(),
                        //     Type::Pat {
                        //         type_,
                        //         __pat_unstable_do_not_use,
                        //     } => todo!(),
                        //     Type::ImplTrait(vec) => todo!(),
                        //     Type::Infer => todo!(),
                        //     Type::RawPointer { is_mutable, type_ } => todo!(),
                        //     Type::BorrowedRef {
                        //         lifetime,
                        //         is_mutable,
                        //         type_,
                        //     } => todo!(),
                        //     Type::QualifiedPath {
                        //         name,
                        //         args,
                        //         self_type,
                        //         trait_,
                        //     } => todo!(),
                        _ => {}
                    }
                }
                // ItemEnum::Enum(_) => todo!(),
                // ItemEnum::Variant(variant) => todo!(),
                // ItemEnum::Function(function) => todo!(),
                // ItemEnum::Trait(t) => {
                //     println!("Found trait {:?}", item.name);
                // }
                // ItemEnum::TraitAlias(trait_alias) => todo!(),
                ItemEnum::Impl(i) => {
                    let Some(trait_) = &i.trait_ else {
                        continue;
                    };

                    let path = self.docs.paths.get(&trait_.id).unwrap();
                    println!("{:?}", path);
                    // TODO: This should be done as `crate_id == cloud_sdk_crate.id` to be cheaper.
                    let crate_info = self.docs.external_crates.get(&path.crate_id).unwrap();

                    let for_ = match &i.for_ {
                        Type::ResolvedPath(path) => path,
                        _ => todo!(),
                    };

                    if crate_info.name == CLOUD_SDK_PKG_NAME_2 {
                        // println!("Found environment struct {:?} {:?}", for_.id, item.id);

                        self.parse_environment_struct(self.docs.index.get(&for_.id).unwrap());
                    }
                }
                // ItemEnum::TypeAlias(type_alias) => todo!(),
                // ItemEnum::Constant { type_, const_ } => todo!(),
                // ItemEnum::Static(_) => todo!(),
                // ItemEnum::ExternType => todo!(),
                // ItemEnum::Macro(_) => todo!(),
                // ItemEnum::ProcMacro(proc_macro) => todo!(),
                // ItemEnum::Primitive(primitive) => todo!(),
                // ItemEnum::AssocConst { type_, value } => todo!(),
                // ItemEnum::AssocType {
                //     generics,
                //     bounds,
                //     type_,
                // } => todo!(),
                _ => {}
            }
        }
    }

    // Parse a struct which was detected to implement `cloud_sdk::Environment`.
    fn parse_environment_struct(&mut self, i: &Item) {
        let s = match &i.inner {
            ItemEnum::Struct(s) => s,
            _ => todo!(),
        };

        // println!("{:?}", i.name.as_ref().unwrap());

        match &s.kind {
            StructKind::Unit => todo!(),
            StructKind::Tuple(_) => todo!(),
            StructKind::Plain { fields, .. } => {
                for field in fields {
                    let got = self.docs.index.get(field).unwrap();
                    let name = got.name.as_ref().unwrap();

                    // println!("\t{:?} {:?}", got.name.as_ref().unwrap(), got.inner);

                    self.manifest
                        .entrypoints
                        .get_mut(&self.binary_name)
                        .unwrap()
                        .environment
                        .insert(
                            name.to_string(),
                            EnvironmentVariable {
                                // TODO: Hook all of this stuff up
                                sealed: Default::default(),
                                optional: Default::default(),
                                default: Default::default(),
                                regex: Default::default(),
                            },
                        );
                }
            }
        }
    }
}

// Ensure that crate names are in canonical form! Damn automated hyphen substitution!
pub fn normalize_crate_name(s: &str) -> String {
    s.replace('-', "_")
}