//! Buf module compilation.

use crate::descriptor::FileDescriptorProto;
use crate::input::{ResolveArgs, ResolvedInput, compile_to_fds, tool_exists};
use crate::paths::{collect_module_proto_names, is_proto_file, proto_name_relative_to_module};
use crate::proto_deps;
use anyhow::{Context, Result, bail};
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;

pub fn compile_with_buf(args: &ResolveArgs, _module_root: &Path) -> Result<ResolvedInput> {
    if args.inputs.is_empty() {
        bail!("no inputs provided for buf compilation");
    }

    let module_root = find_buf_module_root(&args.inputs[0])?.with_context(|| {
        format!(
            "no Buf module (buf.yaml) found for {}; pass a module root or use `--compiler protoc`",
            args.inputs[0].display()
        )
    })?;

    let buf = resolve_buf_path(args.buf_path.as_deref())?;
    let proto_file = compile_to_fds(|fds_path| {
        let status = Command::new(&buf)
            .current_dir(&module_root)
            .args(["build", "-o"])
            .arg(fds_path)
            .status()
            .with_context(|| format!("spawn {}", buf.display()))?;
        if !status.success() {
            bail!("buf build failed in {}", module_root.display());
        }
        Ok(())
    })?;
    let file_to_generate =
        resolve_file_to_generate_for_inputs(&module_root, &args.inputs, &proto_file)?;

    let mut proto_search_paths = vec![module_root.clone()];
    if let Some(export) = &args.proto_deps_export {
        proto_deps::ensure_proto_deps_export(
            &module_root,
            export,
            false,
            args.buf_path.as_deref(),
        )?;
        proto_search_paths.push(export.clone());
    }
    proto_search_paths.extend(args.proto_paths.iter().cloned());

    Ok(ResolvedInput {
        proto_file,
        file_to_generate,
        proto_search_paths,
        module_root,
    })
}

pub fn resolve_buf_path(explicit: Option<&Path>) -> Result<PathBuf> {
    if let Some(path) = explicit {
        return Ok(path.to_path_buf());
    }
    if tool_exists("buf") {
        return Ok(PathBuf::from("buf"));
    }
    #[cfg(feature = "buf")]
    {
        Ok(buf_tools::buf_bin_path())
    }
    #[cfg(not(feature = "buf"))]
    {
        bail!("buf not found on PATH; install with: {BUF_INSTALL_HINT}")
    }
}

fn find_buf_module_root(start: &Path) -> Result<Option<PathBuf>> {
    let start = if start.is_file() {
        start
            .parent()
            .context("input file has no parent directory")?
    } else {
        start
    };
    let mut dir = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
    loop {
        if dir.join("buf.yaml").is_file() || dir.join("buf.yml").is_file() {
            return Ok(Some(dir));
        }
        if !dir.pop() {
            break;
        }
    }
    Ok(None)
}

fn resolve_file_to_generate_for_inputs(
    module_root: &Path,
    inputs: &[PathBuf],
    proto_file: &[FileDescriptorProto],
) -> Result<Vec<String>> {
    let explicit: Vec<PathBuf> = inputs
        .iter()
        .flat_map(|input| {
            if input.is_file() {
                vec![input.clone()]
            } else if input.is_dir() && input == module_root {
                Vec::new()
            } else if input.is_dir() {
                WalkDir::new(input)
                    .into_iter()
                    .filter_map(Result::ok)
                    .filter(|e| is_proto_file(e.path()))
                    .map(|e| e.path().to_path_buf())
                    .collect()
            } else {
                Vec::new()
            }
        })
        .collect();

    if explicit.is_empty() {
        return Ok(collect_module_proto_names(module_root, proto_file));
    }

    let mut names = Vec::new();
    for path in explicit {
        let name = proto_name_relative_to_module(module_root, &path)?;
        if proto_file
            .iter()
            .any(|f| f.name.as_deref() == Some(name.as_str()))
        {
            names.push(name);
        }
    }
    names.sort();
    names.dedup();
    Ok(names)
}
