use std::path::PathBuf;

use super::{parse_file_or_stdin, select_view, write_or_stdout};
use usage::docs::markdown::MarkdownRenderer;
use usage_rs::Args;

/// Generate markdown documentation from usage specs
#[derive(Args)]
#[usage(alias = "md", effect = "read")]
pub struct Markdown {
    /// A usage spec taken in as a file, use "-" to read from stdin
    #[usage(short, long)]
    file: PathBuf,

    /// Render one spec-declared executable view
    #[usage(long)]
    view: Option<String>,
    // /// Pass a usage spec in an argument instead of a file
    // #[usage(short, long, required_unless = "--file", overrides = "--file")]
    // spec: Option<String>,
    /// Render each subcommand as a separate markdown file
    #[usage(short, long, conflicts = "--out-file")]
    multi: bool,

    /// Escape HTML in markdown
    #[usage(long)]
    html_encode: bool,

    /// Output markdown files to this directory (required when using --multi)
    #[usage(
        long,
        value_hint = usage_rs::ValueHint::DirPath,
        requires = "--multi",
        required_if = "--multi",
        effect = "write"
    )]
    out_dir: Option<PathBuf>,

    /// Output file path for single-file markdown generation, or "-" for stdout (default)
    #[usage(
        long,
        value_hint = usage_rs::ValueHint::FilePath,
        effect = "write"
    )]
    out_file: Option<PathBuf>,

    /// Replace `<pre>` tags with markdown code fences
    #[usage(long)]
    replace_pre_with_code_fences: bool,

    /// Prefix to add to all URLs
    #[usage(long)]
    url_prefix: Option<String>,
}

impl usage_rs::Run for Markdown {
    type Output = miette::Result<()>;

    fn run(self) -> Self::Output {
        // The banner belongs to every generated document, so build it in one place rather
        // than once per output path.
        let render = |md: &str| {
            format!(
                "<!-- @generated by usage-cli from usage spec -->\n{}\n",
                md.trim()
            )
        };
        // File-only, deliberately: every path this receives is a join onto `--out-dir`, so a
        // `-` meaning stdout cannot turn up here.
        let write = |path: &PathBuf, md: &str| -> miette::Result<()> {
            eprintln!("writing to {}", path.display());
            super::write_file(path, render(md))?;
            Ok(())
        };
        let spec = select_view(parse_file_or_stdin(&self.file)?, self.view.as_deref())?;
        let mut ctx = MarkdownRenderer::new(spec.clone())
            .with_html_encode(self.html_encode)
            .with_replace_pre_with_code_fences(self.replace_pre_with_code_fences);
        if let Some(url_prefix) = &self.url_prefix {
            ctx = ctx.with_url_prefix(url_prefix);
        }
        if self.multi {
            ctx = ctx.with_multi(true);
            // Before anything is written, so a refusal does not leave a half-built directory.
            //
            // Refused rather than warned about: writing anyway destroys the command's own
            // documentation, and a warning printed into a docs build is a warning nobody reads.
            // No CLI in the fleet has a `configuration` command — they call it `config` — so
            // this asks the one author who ever hits it to pick a name, rather than quietly
            // losing a page for everybody who does not.
            if !ctx.render_config()?.trim().is_empty() {
                if let Some(cmd) = ctx.config_page_collision() {
                    miette::bail!(
                        "the `{cmd}` command's page and the settings page would both be written \
                         to {}; rename the command, or hide it, to generate both",
                        ctx.config_page()
                    );
                }
            }
            let commands = spec.cmd.all_subcommands().into_iter().filter(|c| !c.hide);
            for cmd in commands {
                let md = ctx.render_cmd(cmd)?;
                let dir = cmd
                    .full_cmd
                    .iter()
                    .take(cmd.full_cmd.len() - 1)
                    .map(|c| c.to_string())
                    .collect::<Vec<_>>()
                    .join("/");
                let path = self
                    .out_dir
                    .as_ref()
                    .unwrap()
                    .join(dir)
                    .join(format!("{}.md", cmd.name));
                write(&path, &md)?;
            }
            let md_idx = ctx.render_index()?;
            let path_idx = self.out_dir.as_ref().unwrap().join("index.md");
            write(&path_idx, &md_idx)?;
            // Its own page, and only when there is something to put on it: a CLI with no
            // settings should not gain an empty file in its docs directory.
            let md_config = ctx.render_config()?;
            if !md_config.trim().is_empty() {
                // The same call the index links by, so the two cannot disagree about where the
                // page went.
                let path = self.out_dir.as_ref().unwrap().join(ctx.config_page());
                write(&path, &md_config)?;
            }
        } else {
            let md = ctx.render_spec()?;
            write_or_stdout(self.out_file.as_deref(), &render(&md))?;
        }
        Ok(())
    }
}
