use std::env;
use std::fs;
use std::path::Path;
use std::process::Command;

use anyhow::{bail, Context, Result};

use chrono::Utc;

macro_rules! log_info {
    ($($arg:tt)*) => {
        println!("[INFO] (memex::pipeline): {}", format_args!($($arg)*));
    };
}

use crate::{
    agents::{build_agents_bundle, render_default_markdown, DefaultRenderOptions},
    config::{
        build_config_plan_from_raw, parse_raw_config, resolve_workspace_root, CargoDocConfig,
        ConfigPlan,
    },
    render_targets,
};

/// Build documentation with default options.
///
/// This runs the same pipeline as memex's xtask: it ensures nightly
/// is installed, builds rustdoc JSON for the library crate, renders
/// Markdown via memex, updates `docs/src/SUMMARY.md`, and builds
/// the mdBook site under `docs/book/`.
pub fn build_site() -> Result<()> {
    let cwd = env::current_dir().context("failed to determine current directory")?;
    build_site_with_config_path(cwd.join("memex.toml"))
}

/// Build documentation using a specific `memex.toml`.
///
/// `config_path` may be absolute or relative to the current working directory.
/// The workspace root is derived from `[workspace]` in the config; when omitted,
/// the directory containing the config file is used.
pub fn build_site_with_config_path<P: AsRef<Path>>(config_path: P) -> Result<()> {
    ensure_nightly_toolchain()?;

    let cwd = env::current_dir().context("failed to determine current directory")?;
    let config_path = config_path.as_ref();
    let config_path = if config_path.is_absolute() {
        config_path.to_path_buf()
    } else {
        cwd.join(config_path)
    };
    let config_dir = config_path
        .parent()
        .context("memex.toml has no parent directory")?
        .to_path_buf();

    let cfg_text = fs::read_to_string(&config_path)
        .with_context(|| format!("failed to read `{}`", config_path.display()))?;
    let raw_cfg = parse_raw_config(&cfg_text)?;
    let workspace_root = resolve_workspace_root(&config_dir, &raw_cfg.workspace)?;
    let rustdoc_dir = workspace_root.join("target/doc");
    let plan: ConfigPlan =
        build_config_plan_from_raw(&workspace_root, &config_dir, &rustdoc_dir, raw_cfg)?;
    let cargo_cfg: CargoDocConfig = plan.cargo_doc.clone();

    run_cargo_doc(&workspace_root, &cargo_cfg)?;

    if !rustdoc_dir.exists() {
        bail!("expected rustdoc output at `{}`", rustdoc_dir.display());
    }

    log_info!("Generating API reference documentation...");
    let config = plan.materialize()?;
    let docs = render_targets(&config.targets)?;

    for target in &config.targets {
        let out_dir = config.docs_root.join(&target.out_root);
        if out_dir.exists() {
            fs::remove_dir_all(&out_dir)
                .with_context(|| format!("failed to clean `{}`", out_dir.display()))?;
        }
    }

    for file in &docs.files {
        let file_path = config.docs_root.join(&file.path);
        if let Some(parent) = file_path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("failed to create directory `{}`", parent.display()))?;
        }
        fs::write(&file_path, &file.content)
            .with_context(|| format!("failed to write `{}`", file.path))?;
    }
    log_info!("Generated {} documentation files", docs.files.len());

    update_summary(&config.docs_root.join("SUMMARY.md"), &docs.summary_fragment)?;
    log_info!("Updated SUMMARY.md");

    log_info!("Building mdBook...");
    run_command("mdbook", &["build", "docs"])?;

    if let Some(agents_cfg) = config.agents.as_ref() {
        log_info!("Generating agent documentation bundle...");
        let bundle = build_agents_bundle(&config.docs_root, &config.docs_root_display, agents_cfg)?;

        let mut render_opts = DefaultRenderOptions {
            generated: Some(Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string()),
            git_hash: git_head(&workspace_root)?,
            doc_structure: Some(docs.summary_fragment.clone()),
            doc_structure_source: Some(format!("{}/SUMMARY.md", config.docs_root_display)),
            docs_root_display: config.docs_root_display.clone(),
            ..Default::default()
        };

        if bundle.options.include_tree {
            match crate::agents::generate_repo_tree(&workspace_root, &bundle.options.tree_excludes)
            {
                Ok(tree) => render_opts.repo_tree = Some(tree),
                Err(err) => {
                    println!("[WARN] (memex::pipeline): failed to generate repo tree: {err:#}");
                }
            }
        }

        let markdown = render_default_markdown(&bundle, &render_opts);
        let agents_output = workspace_root.join("AGENTS.md");
        fs::write(&agents_output, markdown)
            .with_context(|| format!("failed to write `{}`", agents_output.display()))?;
        if let Ok(meta) = fs::metadata(&agents_output) {
            let size_bytes = meta.len();
            let size_kb = (size_bytes as f64 / 1024.0).round() as u64;
            log_info!(
                "Wrote agent documentation bundle to {} ({} bytes, {} KB)",
                agents_output.display(),
                size_bytes,
                size_kb
            );
        } else {
            log_info!(
                "Wrote agent documentation bundle to {}",
                agents_output.display()
            );
        }
    }

    log_info!(
        "Docs ready at `{}`",
        workspace_root.join("docs/book/index.html").display()
    );
    Ok(())
}

/// Invoke `cargo +nightly doc` with the requested options.
///
/// Returns an error if the cargo process exits unsuccessfully.
fn run_cargo_doc(workspace_root: &Path, options: &CargoDocConfig) -> Result<()> {
    log_info!("Running cargo +nightly doc (JSON)...");

    let mut args: Vec<&str> = vec!["+nightly", "doc"];
    if options.workspace {
        args.push("--workspace");
    } else {
        args.push("--lib");
    }
    if options.no_deps {
        args.push("--no-deps");
    }
    if options.document_private_items {
        args.push("--document-private-items");
    }

    let status = Command::new("cargo")
        .current_dir(workspace_root)
        .env("RUSTDOCFLAGS", "-Z unstable-options --output-format json")
        .args(args)
        .status()
        .context("failed to run cargo doc")?;
    if status.success() {
        Ok(())
    } else {
        bail!("command `cargo +nightly doc` exited with status {status}");
    }
}

/// Spawn a process and ensure it exits successfully.
///
/// Used for helper commands such as `mdbook build`.
fn run_command(cmd: &str, args: &[&str]) -> Result<()> {
    let status = Command::new(cmd)
        .args(args)
        .status()
        .with_context(|| format!("failed to spawn `{cmd}`"))?;
    if status.success() {
        Ok(())
    } else {
        bail!("command `{cmd}` exited with status {status}");
    }
}

/// Ensure the nightly Rust toolchain is installed.
///
/// If `rustup run nightly rustc --version` fails, installs nightly
/// via `rustup toolchain install nightly`.
fn ensure_nightly_toolchain() -> Result<()> {
    let check = Command::new("rustup")
        .args(["run", "nightly", "rustc", "--version"])
        .status();
    match check {
        Ok(status) if status.success() => Ok(()),
        _ => {
            log_info!("Installing nightly toolchain for rustdoc JSON support...");
            run_command("rustup", &["toolchain", "install", "nightly"])
        }
    }
}

/// Merge the generated API fragment into `SUMMARY.md`.
///
/// The fragment replaces an existing `API Reference` section if one
/// exists, otherwise it appends a new section at the end.
fn update_summary(summary_path: &Path, api_fragment: &str) -> Result<()> {
    let content = fs::read_to_string(summary_path)
        .with_context(|| format!("failed to read `{}`", summary_path.display()))?;

    let lines: Vec<&str> = content.lines().collect();

    let mut api_index = None;
    let mut next_top_level = None;
    for (i, line) in lines.iter().enumerate() {
        if line.contains("[API Reference]") {
            api_index = Some(i);
            for (j, next_line) in lines.iter().enumerate().skip(i + 1) {
                if next_line.starts_with("- [") {
                    next_top_level = Some(j);
                    break;
                }
            }
            break;
        }
    }

    let new_content = match api_index {
        Some(idx) => {
            let before = &lines[..idx];
            let after = match next_top_level {
                Some(next_idx) => &lines[next_idx..],
                None => &lines[lines.len()..],
            };
            let mut result = before.join("\n");
            if !result.is_empty() {
                result.push('\n');
            }
            result.push_str(api_fragment.trim_end());
            result.push('\n');
            if !after.is_empty() {
                result.push_str(&after.join("\n"));
                result.push('\n');
            }
            result
        }
        None => {
            let mut result = content.trim_end().to_string();
            if !result.is_empty() {
                result.push('\n');
            }
            result.push_str(api_fragment.trim_end());
            result.push('\n');
            result
        }
    };

    fs::write(summary_path, new_content)
        .with_context(|| format!("failed to write `{}`", summary_path.display()))
}

/// Retrieve the current git HEAD hash (first 8 characters) for logging purposes.
fn git_head(workspace_root: &Path) -> Result<Option<String>> {
    let output = Command::new("git")
        .args(["rev-parse", "HEAD"])
        .current_dir(workspace_root)
        .output();

    match output {
        Ok(output) if output.status.success() => {
            let hash = String::from_utf8_lossy(&output.stdout)
                .trim()
                .chars()
                .take(8)
                .collect::<String>();
            if hash.is_empty() {
                Ok(None)
            } else {
                Ok(Some(hash))
            }
        }
        _ => Ok(None),
    }
}
