//! Path normalization and proto file discovery.

use anyhow::{Context, Result, bail};
use std::path::{Component, Path, PathBuf};
use walkdir::WalkDir;

use crate::descriptor::FileDescriptorProto;

pub fn normalize_rel_dir(path: &Path) -> PathBuf {
    path.components()
        .filter_map(|c| match c {
            Component::Normal(s) => Some(s.to_owned()),
            _ => None,
        })
        .collect()
}

pub fn is_proto_file(path: &Path) -> bool {
    path.is_file() && path.extension().and_then(|e| e.to_str()) == Some("proto")
}

pub fn proto_name_from_path(path: &Path) -> Result<String> {
    let name = path.to_string_lossy().replace('\\', "/");
    if name.contains("..") {
        bail!("proto path must not contain `..`: {name}");
    }
    Ok(name.trim_start_matches("./").to_string())
}

pub fn proto_name_relative_to_module(module_root: &Path, path: &Path) -> Result<String> {
    let module_root = module_root
        .canonicalize()
        .unwrap_or_else(|_| module_root.to_path_buf());
    let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
    let rel = path.strip_prefix(&module_root).with_context(|| {
        format!(
            "{} is not under module root {}",
            path.display(),
            module_root.display()
        )
    })?;
    let mut parts = Vec::new();
    for c in rel.components() {
        match c {
            Component::Normal(s) => parts.push(s.to_string_lossy().into_owned()),
            Component::CurDir => {}
            other => bail!("invalid path component in {}: {other:?}", path.display()),
        }
    }
    Ok(parts.join("/"))
}

pub fn collect_proto_inputs(inputs: &[PathBuf]) -> Result<(Vec<PathBuf>, Vec<String>)> {
    let mut rel_files = Vec::new();
    let mut generate_names = Vec::new();

    for input in inputs {
        if input.is_file() {
            if input.extension().and_then(|e| e.to_str()) != Some("proto") {
                bail!("not a .proto file: {}", input.display());
            }
            let name = proto_name_from_path(input)?;
            rel_files.push(input.to_path_buf());
            generate_names.push(name);
            continue;
        }
        if input.is_dir() {
            for entry in WalkDir::new(input).into_iter().filter_map(Result::ok) {
                let path = entry.path();
                if is_proto_file(path) {
                    let name = proto_name_from_path(path)?;
                    rel_files.push(path.to_path_buf());
                    generate_names.push(name);
                }
            }
            continue;
        }
        bail!("input not found: {}", input.display());
    }

    rel_files.sort();
    rel_files.dedup();
    generate_names.sort();
    generate_names.dedup();
    Ok((rel_files, generate_names))
}

pub fn collect_module_proto_names(
    module_root: &Path,
    proto_file: &[FileDescriptorProto],
) -> Vec<String> {
    let module_root = module_root
        .canonicalize()
        .unwrap_or_else(|_| module_root.to_path_buf());
    let mut names = Vec::new();
    for entry in WalkDir::new(&module_root)
        .into_iter()
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if !is_proto_file(path) {
            continue;
        }
        if let Ok(name) = proto_name_relative_to_module(&module_root, path)
            && proto_file
                .iter()
                .any(|f| f.name.as_deref() == Some(name.as_str()))
        {
            names.push(name);
        }
    }
    names.sort();
    names.dedup();
    names
}
