//! `ctxforge add` — add files, globs, line-ranges, functions, types, or
//! diff'd files to the bundle.

use crate::bundle::{Bundle, Item};
use crate::error::{CtxforgeError, Result};
use crate::output;
use crate::paths::CtxforgeRoot;
use crate::source::{FileSource, FuncSource, Source, TypeSource};
use crate::walk;
use std::path::{Path, PathBuf};

#[allow(clippy::too_many_arguments)]
pub fn run(
    root: &CtxforgeRoot,
    _cwd: &Path,
    patterns: Vec<String>,
    exclude: Vec<String>,
    diff: Option<String>,
    functions: Vec<String>,
    types: Vec<String>,
    allow_http: bool,
    allow_private_net: bool,
) -> Result<()> {
    let project_root = root.project_root().to_path_buf();
    let mut bundle = Bundle::load_or_default(root)?;
    let mut added_count: usize = 0;

    if !functions.is_empty() {
        let file_path = require_single_file(&patterns, "--fn")?;
        let pb = output::spinner(&format!("scanning {file_path} for functions..."));
        #[cfg(feature = "extract")]
        validate_symbols(
            &project_root,
            &file_path,
            &functions,
            crate::extract::extract_function,
            "function",
        );
        for name in &functions {
            let item = Item {
                source: Source::Func(FuncSource {
                    path: PathBuf::from(&file_path),
                    name: name.clone(),
                }),
                label: None,
            };
            bundle.add(item);
            added_count += 1;
        }
        pb.finish_and_clear();
    }

    if !types.is_empty() {
        let file_path = require_single_file(&patterns, "--type")?;
        let pb = output::spinner(&format!("scanning {file_path} for types..."));
        #[cfg(feature = "extract")]
        validate_symbols(
            &project_root,
            &file_path,
            &types,
            crate::extract::extract_type,
            "type",
        );
        for name in &types {
            let item = Item {
                source: Source::Type(TypeSource {
                    path: PathBuf::from(&file_path),
                    name: name.clone(),
                }),
                label: None,
            };
            bundle.add(item);
            added_count += 1;
        }
        pb.finish_and_clear();
    }

    if !functions.is_empty() || !types.is_empty() {
        bundle.save(root)?;
        output::success(&format!(
            "added {added_count} item(s); bundle now has {}",
            bundle.len()
        ));
        return Ok(());
    }

    if let Some(branch) = diff {
        let changed = crate::git::changed_files(&project_root, &branch)?;
        for p in changed {
            let item = Item {
                source: Source::File(FileSource { path: p }),
                label: None,
            };
            if item_matches_exclude(&item, &exclude)? {
                continue;
            }
            bundle.add(item);
            added_count += 1;
        }
    }

    for pat in &patterns {
        if is_uri_pattern(pat) {
            let item = Item::parse_add_argument(pat)?;
            if let Source::Url(u) = &item.source {
                crate::source::url::validate_url(&u.url, allow_http, allow_private_net)
                    .map_err(CtxforgeError::Msg)?;
            }
            bundle.add(item);
            added_count += 1;
            continue;
        }

        if looks_like_ranged_path(pat) {
            let item = Item::parse_add_argument(pat)?;
            if item_matches_exclude(&item, &exclude)? {
                continue;
            }
            bundle.add(item);
            added_count += 1;
            continue;
        }

        let paths = walk::expand(pat, &project_root, &exclude)?;
        if paths.is_empty() {
            if !pat.contains('*') && !pat.contains('?') && !pat.contains('[') && !pat.contains('{')
            {
                let target = project_root.join(pat);
                let parent = target.parent().unwrap_or(&project_root);
                let siblings: Vec<String> = std::fs::read_dir(parent)
                    .ok()
                    .into_iter()
                    .flatten()
                    .filter_map(|entry| entry.ok())
                    .filter_map(|entry| entry.file_name().into_string().ok())
                    .collect();
                let query = target.file_name().and_then(|n| n.to_str()).unwrap_or("");
                let suggestions = output::close_matches(query, &siblings, 3);
                return Err(CtxforgeError::NotFound {
                    path: std::path::PathBuf::from(pat),
                    suggestions,
                });
            }
            output::warn(&format!("`{pat}` matched no files"));
        }
        for p in paths {
            let item = Item {
                source: Source::File(FileSource { path: p }),
                label: None,
            };
            bundle.add(item);
            added_count += 1;
        }
    }

    if patterns.is_empty() && added_count == 0 {
        return Err(CtxforgeError::Msg(
            "ctxforge add: no patterns provided (use --diff, --fn, --type, or pass path arguments)"
                .into(),
        ));
    }

    bundle.save(root)?;
    output::success(&format!(
        "added {added_count} item(s); bundle now has {}",
        bundle.len()
    ));
    Ok(())
}

fn is_uri_pattern(pat: &str) -> bool {
    pat.starts_with("https://")
        || pat.starts_with("http://")
        || pat.starts_with("url:")
        || pat.starts_with("file://")
        || pat.starts_with("range://")
        || pat.starts_with("func:")
        || pat.starts_with("type:")
        || pat.starts_with("gh://")
}

#[cfg(feature = "extract")]
fn validate_symbols(
    project_root: &Path,
    file_path: &str,
    names: &[String],
    extractor: fn(&str, &str, &str) -> std::result::Result<Option<String>, String>,
    kind_label: &str,
) {
    let abs = project_root.join(file_path);
    let Ok(source) = std::fs::read_to_string(&abs) else {
        return;
    };
    let language = crate::lang::detect(Path::new(file_path));
    for name in names {
        match extractor(&source, language, name) {
            Ok(Some(_)) => {}
            Ok(None) => output::warn(&format!(
                "{kind_label} `{name}` not found in {file_path} (added anyway)"
            )),
            Err(_) => {}
        }
    }
}

fn require_single_file(patterns: &[String], flag: &str) -> Result<String> {
    if patterns.len() != 1 {
        return Err(CtxforgeError::Msg(format!(
            "{flag} requires exactly one file path (got {})",
            patterns.len()
        )));
    }
    Ok(patterns[0].clone())
}

fn looks_like_ranged_path(s: &str) -> bool {
    if let Some((_, after)) = s.rsplit_once(':') {
        after.contains('-') && after.chars().all(|c| c.is_ascii_digit() || c == '-')
    } else {
        false
    }
}

fn item_matches_exclude(item: &Item, excludes: &[String]) -> Result<bool> {
    let Some(path) = item.source.display_path() else {
        return Ok(false);
    };
    exclude_matches(path.as_path(), excludes)
}

fn exclude_matches(path: &Path, excludes: &[String]) -> Result<bool> {
    use globset::{Glob, GlobSetBuilder};
    if excludes.is_empty() {
        return Ok(false);
    }
    let mut b = GlobSetBuilder::new();
    for e in excludes {
        b.add(Glob::new(e)?);
    }
    Ok(b.build()?.is_match(path))
}
