mod admin_commands;

use std::{collections::HashMap, fs::File, io::Write, path::{Path, PathBuf}};

use cargo_metadata::MetadataCommand;

use crate::tasks::TaskResult;

trait FileOutput {
    fn create_file(&mut self, path: PathBuf, contents: String);
}

#[derive(Default)]
struct FileQueue {
    queue: HashMap<PathBuf, String>,
}

impl FileQueue {
    const GENERATED_HEADER: &'static str = "This file is generated by `cargo xtask generate-docs`. Do not edit.";

    /// Generate the header for the file at `path`.
    fn header_for(path: &Path) -> Option<String> {
        Some(match path.extension()?.as_encoded_bytes() {
            b"md" => format!("<!-- {} -->", Self::GENERATED_HEADER),
            _ => return None
        })
    }

    /// Write all queued files into the file tree rooted at `root`.
    fn write(self, root: &Path, dry_run: bool) -> std::io::Result<()> {
        for (path, contents) in self.queue {
            let path = root.join(&path);

            eprintln!("Writing {}", path.display());
            if !dry_run {
                let mut file = File::create(&path)?;

                if let Some(header) = Self::header_for(&path) {
                    writeln!(file, "{header}")?;
                }

                write!(file, "{contents}")?;
                if !contents.ends_with('\n') {
                    writeln!(file)?;
                }
            }
        }

        Ok(())
    }
}

impl FileOutput for FileQueue {
    fn create_file(&mut self, path: PathBuf, contents: String) {
        assert!(path.is_relative(), "path must be relative");
        assert!(path.extension().is_some(), "path must not point to a directory");

        assert!(!self.queue.contains_key(&path), "attempted to create an already created file {}", path.display());

        self.queue.insert(path, contents);
    }
}

#[derive(clap::Args)]
pub(crate) struct Args {
    /// The base path of the documentation. Defaults to `docs/` in the crate root.
    root: Option<PathBuf>,
}

#[expect(clippy::needless_pass_by_value)]
pub(super) fn run(common_args: crate::Args, task_args: Args) -> TaskResult<()> {
    let mut queue = FileQueue::default();

    let metadata = MetadataCommand::new()
        .no_deps()
        .exec()
        .expect("should have been able to run cargo");

    let root = task_args.root.unwrap_or_else(|| metadata.workspace_root.join_os("docs/"));

    admin_commands::generate(&mut queue)?;

    queue.write(&root, common_args.dry_run)?;

    Ok(())
}
