use crate::names::upstream_package_to_debian_source_name;
use crate::Error;
use breezyshim::branch::Branch;
use breezyshim::workingtree::PyWorkingTree;
use debian_analyzer::debhelper::maximum_debhelper_compat_version;
use debian_analyzer::editor::{Editor, TreeEditor};
use debian_analyzer::lintian::latest_standards_version;
use debian_analyzer::relations::{ensure_exact_version, ensure_relation, ensure_some_version};
use debian_control::fields::MultiArch;
use debian_control::lossless::relations::Relations;
use debian_control::lossless::{Control, Source};
use debversion::Version;
use ognibuild::buildsystem::BuildSystem;
use ognibuild::debian::upstream_deps::get_project_wide_deps;
use ognibuild::session::Session;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use upstream_ontologist::UpstreamMetadata;
use url::Url;

struct ProcessorContext<'a> {
    session: &'a dyn Session,
    wt: &'a dyn PyWorkingTree,
    subpath: PathBuf,
    debian_path: PathBuf,
    upstream_version: String,
    metadata: &'a UpstreamMetadata,
    compat_release: String,
    buildsystem: Box<dyn BuildSystem>,
    _buildsystem_subpath: PathBuf,
    maintainer: Option<String>,
    _kickstart_from_dist: Option<Box<dyn FnOnce(&dyn PyWorkingTree, &Path) -> Result<(), Error>>>,
}

impl<'a> ProcessorContext<'a> {
    fn kickstart_tree(&mut self, sourceful: bool) -> Result<(), Error> {
        if sourceful {
            if let Some(kickstart_fn) = self._kickstart_from_dist.take() {
                kickstart_fn(self.wt, &self.subpath)?;
            } else {
                // If no kickstart function is provided, just ensure we have a clean tree
                log::debug!("No kickstart_from_dist function provided, skipping dist import");
            }
        } else {
            prepare_empty_subpath(self.wt, &self.subpath)?;
        }
        Ok(())
    }

    fn create_control_file(&self) -> Result<TreeEditor<'_, Control>, Error> {
        Ok(TreeEditor::<Control>::new(
            self.wt,
            &self.debian_path.join("control"),
            false,
            true,
        )?)
    }

    fn bootstrap_debhelper(
        &self,
        source: &mut Source,
        config: DebhelperConfig,
    ) -> Result<(), Error> {
        bootstrap_debhelper(
            self.wt,
            &self.debian_path,
            source,
            &self.compat_release,
            config,
        )
    }

    fn get_project_wide_deps(&self) -> (Relations, Relations) {
        // Use the ognibuild dependency resolution with the provided session
        let (build_deps, test_deps) =
            get_project_wide_deps(self.session, self.buildsystem.as_ref());

        let mut build_ret = Relations::new();
        for dep in build_deps {
            let rs: Relations = dep.into();
            for rel in rs.entries() {
                ensure_relation(&mut build_ret, rel);
            }
        }
        let mut test_ret = Relations::new();
        for dep in test_deps {
            let rs: Relations = dep.into();
            for rel in rs.entries() {
                ensure_relation(&mut test_ret, rel);
            }
        }
        (build_ret, test_ret)
    }
}

/// Prepare an empty tree for a non-sourceful debianization.
///
/// Resets the branch to an empty revision history and ensures `subpath` exists
/// as a versioned directory in the working tree.
fn prepare_empty_subpath(wt: &dyn PyWorkingTree, subpath: &Path) -> Result<(), Error> {
    wt.branch()
        .generate_revision_history(&breezyshim::RevisionId::null())
        .unwrap();
    if !wt.has_filename(subpath) {
        wt.mkdir(subpath)?;
    }
    if !wt.is_versioned(subpath) {
        wt.add(&[subpath])?;
    }
    Ok(())
}

fn enable_dh_addon(source: &mut Source, addon: &str) {
    let mut build_depends = source.build_depends().unwrap_or_default();
    ensure_some_version(&mut build_depends, &format!("dh-sequence-{}", addon));
    source.set_build_depends(&build_depends);
}

fn import_build_deps(source: &mut Source, new_build_deps: &Relations) {
    let mut build_depends = source.build_depends().unwrap_or_default();
    for build_dep in new_build_deps.entries() {
        for rel in build_dep.relations() {
            ensure_relation(&mut build_depends, rel.into());
        }
    }
    source.set_build_depends(&build_depends);
}

fn debhelper_rules<F: std::io::Write>(
    f: &mut F,
    buildsystem: Option<&str>,
    build_directory: Option<&str>,
    env: HashMap<&str, &str>,
) -> std::io::Result<()> {
    f.write_all(b"#!/usr/bin/make -f\n")?;
    f.write_all(b"%:\n")?;
    f.write_all(b"\tdh $@")?;
    if let Some(buildsystem) = buildsystem {
        f.write_all(format!(" --buildsystem={}", buildsystem).as_bytes())?;
    }
    if let Some(build_directory) = build_directory {
        f.write_all(format!(" --builddirectory={}", build_directory).as_bytes())?;
    }
    f.write_all(b"\n")?;
    for (key, value) in env {
        f.write_all(format!("export {}={}\n", key, value).as_bytes())?;
    }
    Ok(())
}

#[derive(Debug, Default)]
struct DebhelperConfig<'a> {
    addons: Vec<&'a str>,
    env: HashMap<&'a str, &'a str>,
    buildsystem: Option<&'a str>,
    build_directory: Option<&'a str>,
}

fn bootstrap_debhelper(
    wt: &dyn PyWorkingTree,
    debian_path: &Path,
    source: &mut Source,
    compat_release: &str,
    config: DebhelperConfig,
) -> Result<(), Error> {
    let mut build_depends = source.build_depends().unwrap_or_default();
    ensure_exact_version(
        &mut build_depends,
        "debhelper-compat",
        &maximum_debhelper_compat_version(compat_release)
            .to_string()
            .parse::<Version>()
            .unwrap(),
        None,
    );
    source.set_build_depends(&build_depends);
    for addon in config.addons.iter() {
        enable_dh_addon(source, addon);
    }

    let mut f = Vec::new();
    debhelper_rules(
        &mut f,
        config.buildsystem,
        config.build_directory,
        config.env,
    )?;
    wt.put_file_bytes_non_atomic(&debian_path.join("rules"), &f)?;
    Ok(())
}

fn process_setup_py(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine name from setup.py for the python3 project".to_string(),
            ))
        }
    };
    let source_name = crate::names::python_source_package_name(upstream_name);
    let mut source = control.add_source(&source_name);
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("python"));
    source.set_rules_requires_root(false);
    source.set_standards_version(&latest_standards_version().to_string());
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            buildsystem: Some("pybuild"),
            addons: vec!["python3"],
            ..Default::default()
        },
    )?;
    source.set_testsuite("autopkgtest-pkg-python");

    // Check whether project supports Python 3
    let python3_support = check_python3_support(context.wt, &context.subpath)?;
    if !python3_support {
        log::warn!("Project may not support Python 3, but proceeding with Python 3 packaging");
    }

    let mut build_depends = source.build_depends().unwrap_or_default();
    ensure_relation(&mut build_depends, "python3-all".parse().unwrap());
    if context
        .wt
        .has_filename(&context.subpath.join("pyproject.toml"))
    {
        // pybuild needs this plugin to drive PEP 517 build backends.
        ensure_relation(
            &mut build_depends,
            "pybuild-plugin-pyproject".parse().unwrap(),
        );
    }
    if context.wt.has_filename(&context.subpath.join("setup.py")) {
        ensure_relation(&mut build_depends, "python3-setuptools".parse().unwrap());
    }
    source.set_build_depends(&build_depends);
    let (build_deps, test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    // We're going to be running the testsuite as part of the build, so import the test dependencies too.
    import_build_deps(&mut source, &test_deps);
    let binary_name = crate::names::python_binary_package_name(upstream_name);
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("all"));
    // Use raw deb822 field for substitution variables
    binary
        .as_mut_deb822()
        .insert("Depends", "${python3:Depends}");
    control.commit()?;
    Ok(())
}

fn process_maven(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the artifactId from pom.xml for the java project".to_string(),
            ))
        }
    };
    // The pom.xml name can be a human-readable string like "Gson Parent".
    let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
        .unwrap_or_else(|| upstream_name.to_string());
    let mut source = control.add_source(&source_name);
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("java"));
    source.set_rules_requires_root(false);
    source.set_standards_version(&latest_standards_version().to_string());
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            buildsystem: Some("maven"),
            ..Default::default()
        },
    )?;
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    let mut binary = control.add_binary(&format!("lib{}-java", source_name));
    binary.set_architecture(Some("all"));
    // Use a raw deb822 field: substitution variables do not parse as relations
    binary.as_mut_deb822().insert("Depends", "${java:Depends}");
    control.commit()?;
    Ok(())
}

fn process_npm(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name
            .trim_end_matches("@")
            .replace(['/', '_'], "-")
            .replace("@", "")
            .to_lowercase(),
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name from package.json for the node project".to_string(),
            ))
        }
    };

    let mut source = control.add_source(&format!("node-{}", upstream_name));
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            addons: vec!["nodejs"],
            ..Default::default()
        },
    )?;
    source.set_section(Some("javascript"));
    source.set_rules_requires_root(false);
    source.set_standards_version(&latest_standards_version().to_string());
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    let mut binary = control.add_binary(&format!("node-{}", upstream_name));
    binary.set_architecture(Some("all"));
    source.set_testsuite("autopkgtest-pkg-nodejs");
    control.commit()?;
    Ok(())
}

fn process_dist_zilla(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name in dist.ini for the perl project".to_string(),
            ))
        }
    };
    let mut source = control.add_source(&crate::names::perl_package_name(upstream_name));
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("perl"));
    source.set_rules_requires_root(false);
    source.set_testsuite("autopkgtest-pkg-perl");
    source.set_standards_version(&latest_standards_version().to_string());
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            addons: vec!["dist-zilla"],
            ..Default::default()
        },
    )?;
    let binary_name = source.name().unwrap();
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("all"));
    binary.as_mut_deb822().insert("Depends", "${perl:Depends}");
    control.commit()?;
    Ok(())
}

fn process_meson(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name in meson.build".to_string(),
            ))
        }
    };
    let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
        .unwrap_or_else(|| upstream_name.to_string());

    let mut source = control.add_source(&source_name);
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("misc"));
    source.set_rules_requires_root(false);
    source.set_standards_version(&latest_standards_version().to_string());
    source.set_build_depends(&"meson".parse().unwrap());
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            buildsystem: Some("meson"),
            build_directory: Some("build"),
            ..Default::default()
        },
    )?;
    let binary_name = source.name().unwrap();
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("any"));
    binary
        .as_mut_deb822()
        .insert("Depends", "${misc:Depends}, ${shlibs:Depends}");
    control.commit()?;
    Ok(())
}

fn process_perl_build_tiny(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name in dist.ini for the perl project".to_string(),
            ))
        }
    };
    let mut source = control.add_source(&crate::names::perl_package_name(upstream_name));
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("perl"));
    source.set_rules_requires_root(false);
    source.set_testsuite("autopkgtest-pkg-perl");
    source.set_standards_version(&latest_standards_version().to_string());
    source.set_build_depends(&"libmodule-build-perl".parse().unwrap());
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    context.bootstrap_debhelper(&mut source, DebhelperConfig::default())?;
    let binary_name = source.name().unwrap();
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("all"));
    binary.as_mut_deb822().insert("Depends", "${perl:Depends}");
    control.commit()?;
    Ok(())
}

fn process_golang(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;

    let repository_url = match context.metadata.repository() {
        Some(url) => url.parse::<url::Url>().unwrap(),
        None => {
            return Err(Error::MissingUpstreamInfo(
                "no upstream or origin url is associated with the go project".to_string(),
            ))
        }
    };

    let godebname = crate::names::go_base_name(
        &[repository_url.host_str().unwrap(), repository_url.path()].concat(),
    );
    let mut source = control.add_source(&format!("golang-{}", godebname));
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_rules_requires_root(false);
    source.set_standards_version(&latest_standards_version().to_string());
    source.as_mut_deb822().insert(
        "XS-Go-Import-Path",
        &crate::names::go_import_path_from_repo(&repository_url),
    );
    if let Some(url) = context.metadata.repository() {
        source.set_vcs_browser(Some(url));
    }
    source.set_section(Some("devel"));
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    source.set_testsuite("autopkgtest-pkg-go");
    let mut dh_env = HashMap::new();
    if context.wt.has_filename(&context.subpath.join("examples")) {
        dh_env.insert("DH_GOLANG_EXCLUDES", "examples/");
    }
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            addons: vec!["golang"],
            buildsystem: Some("golang"),
            build_directory: Some("_build"),
            env: dh_env,
        },
    )?;
    let mut binary = control.add_binary(&format!("golang-{}-dev", godebname));

    binary.set_architecture(Some("all"));
    binary.set_multi_arch(Some(MultiArch::Foreign));
    control.commit()?;
    Ok(())
}

fn process_r(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;

    let archive = match context.metadata.archive() {
        Some("CRAN") => "cran",
        Some("Bioconductor") => "bioc",
        _ => "other",
    };

    let upstream_name = match context.metadata.name() {
        Some(name) => name.to_lowercase(),
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name from DESCRIPTION for the R project".to_string(),
            ))
        }
    };

    let mut source = control.add_source(&format!("r-{}-{}", archive, upstream_name));
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("gnu-r"));
    source.set_rules_requires_root(false);
    source.set_build_depends(&"dh-r, r-base-dev".parse().unwrap());
    source.set_standards_version(&latest_standards_version().to_string());
    source.set_testsuite("autopkgtest-pkg-r");
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            buildsystem: Some("R"),
            ..Default::default()
        },
    )?;
    // For now, just assume a single binary package that is architecture-dependent.
    let mut binary = control.add_binary(&format!("r-{}-{}", archive, upstream_name));
    binary.set_architecture(Some("any"));
    binary.as_mut_deb822().insert(
        "Depends",
        "${R:Depends}, ${shlibs:Depends}, ${misc:Depends}",
    );
    binary
        .as_mut_deb822()
        .insert("Recommends", "${R:Recommends}");
    binary.as_mut_deb822().insert("Suggests", "${R:Suggests}");
    control.commit()?;
    Ok(())
}

fn process_octave(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name.to_lowercase(),
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name from DESCRIPTION for the octave project".to_string(),
            ))
        }
    };
    let mut source = control.add_source(&format!("octave-{}", upstream_name));
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("math"));
    source.set_rules_requires_root(false);
    source.set_build_depends(&"dh-octave".parse().unwrap());
    source.set_standards_version(&latest_standards_version().to_string());
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            buildsystem: Some("octave"),
            addons: vec!["octave"],
            ..Default::default()
        },
    )?;
    // For now, just assume a single binary package that is architecture-independent.
    let mut binary = control.add_binary(&format!("octave-{}", upstream_name));
    binary.set_architecture(Some("all"));
    binary
        .as_mut_deb822()
        .insert("Depends", "${octave:Depends}, ${misc:Depends}");
    binary.set_description(Some("${octave:Upstream-Description}"));
    control.commit()?;
    Ok(())
}

fn process_default(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the upstream name".to_string(),
            ))
        }
    };
    let source_name =
        crate::names::upstream_name_to_debian_source_name(upstream_name).ok_or_else(|| {
            Error::MissingUpstreamInfo(format!(
                "Unable to determine source package name for {}",
                upstream_name
            ))
        })?;
    let mut source = control.add_source(&source_name);
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("misc"));
    source.set_rules_requires_root(false);
    source.set_standards_version(&latest_standards_version().to_string());
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    context.bootstrap_debhelper(&mut source, DebhelperConfig::default())?;
    // For now, just assume a single binary package that is architecture-dependent.
    let binary_name = source.name().unwrap();
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("any"));
    binary
        .as_mut_deb822()
        .insert("Depends", "${misc:Depends}, ${shlibs:Depends}");
    control.commit()?;
    Ok(())
}

fn process_cmake(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name of the cmake project".to_string(),
            ))
        }
    };
    let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
        .unwrap_or_else(|| upstream_name.to_string());

    let mut source = control.add_source(&source_name);
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_standards_version(&latest_standards_version().to_string());
    source.set_section(Some("misc"));
    source.set_rules_requires_root(false);

    // CMake-specific build dependencies
    let mut build_depends = Relations::new();
    ensure_some_version(&mut build_depends, "cmake");
    source.set_build_depends(&build_depends);

    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            buildsystem: Some("cmake"),
            ..Default::default()
        },
    )?;

    let (additional_build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &additional_build_deps);

    // Add binary package
    let binary_name = source.name().unwrap();
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("any"));
    binary
        .as_mut_deb822()
        .insert("Depends", "${misc:Depends}, ${shlibs:Depends}");

    control.commit()?;
    Ok(())
}

fn process_make(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;

    // The ognibuild make buildsystem also claims ExtUtils::MakeMaker projects.
    if context
        .wt
        .has_filename(&context.subpath.join("Makefile.PL"))
    {
        return process_makefile_pl(context);
    }

    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name of the make project".to_string(),
            ))
        }
    };
    let source_name = crate::names::upstream_name_to_debian_source_name(upstream_name)
        .unwrap_or_else(|| upstream_name.to_string());

    let mut source = control.add_source(&source_name);
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_standards_version(&latest_standards_version().to_string());
    source.set_section(Some("misc"));
    source.set_rules_requires_root(false);

    // The ognibuild make buildsystem also claims autotools trees. Those have
    // no Makefile until configure has run, so forcing the makefile
    // buildsystem would break the build; dh autodetects autoconf.
    let autotools = ["configure.ac", "configure.in", "Makefile.am", "autogen.sh"]
        .iter()
        .any(|f| context.wt.has_filename(&context.subpath.join(f)));
    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            buildsystem: if autotools { None } else { Some("makefile") },
            ..Default::default()
        },
    )?;

    let (additional_build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &additional_build_deps);

    // Add binary package
    let binary_name = source.name().unwrap();
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("any"));
    binary
        .as_mut_deb822()
        .insert("Depends", "${misc:Depends}, ${shlibs:Depends}");

    control.commit()?;
    Ok(())
}

fn process_makefile_pl(context: &mut ProcessorContext) -> Result<(), Error> {
    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name of the perl project".to_string(),
            ))
        }
    };
    let mut source = control.add_source(&crate::names::perl_package_name(upstream_name));
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    source.set_section(Some("perl"));
    source.set_rules_requires_root(false);
    source.set_testsuite("autopkgtest-pkg-perl");
    source.set_standards_version(&latest_standards_version().to_string());
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    // dh autodetects perl_makemaker from Makefile.PL; forcing the makefile
    // buildsystem would skip running Makefile.PL entirely.
    context.bootstrap_debhelper(&mut source, DebhelperConfig::default())?;
    let binary_name = source.name().unwrap();
    let mut binary = control.add_binary(&binary_name);
    binary.set_architecture(Some("all"));
    binary.as_mut_deb822().insert("Depends", "${perl:Depends}");
    control.commit()?;
    Ok(())
}

/// Populate the `[features]` section of a Cargo.toml document.
///
/// If the document has a `features` entry that is not a table (for example,
/// because the upstream Cargo.toml left it as some other value), it is left
/// untouched rather than panicking.
fn populate_cargo_features(
    cargo: &mut toml_edit::DocumentMut,
    features: &HashMap<String, Vec<String>>,
) {
    if let Some(features_section) = cargo["features"].as_table_mut() {
        for (feature, reqs) in features.iter() {
            features_section[feature] = toml_edit::value(toml_edit::Array::new());

            for req in reqs.iter() {
                features_section[feature]
                    .as_array_mut()
                    .unwrap()
                    .push(toml_edit::Value::from(req.to_string()));
            }
        }
    }
}

/// Pick the version string to look up on crates.io.
///
/// For snapshot upstream versions (an upstream version of `"0"`, or one that
/// contains `"git"`), the Debian-derived version is not a real release, so the
/// version recorded in the upstream metadata is preferred, falling back to
/// `"0.0.0"` when none is recorded. Otherwise the Debian-derived base version is
/// used as-is.
fn cargo_version_to_use<'a>(base_version: &'a str, metadata_version: Option<&'a str>) -> &'a str {
    if base_version == "0" || base_version.contains("git") {
        metadata_version.unwrap_or("0.0.0")
    } else {
        base_version
    }
}

/// Determine whether `available` is a newer release series than `desired`.
///
/// Only the major and minor components are compared: a crate published with a
/// higher major-or-minor version than the one being packaged needs a semver
/// suffix in Debian.
fn crate_version_is_newer(available: &semver::Version, desired: &semver::Version) -> bool {
    (available.major, available.minor) > (desired.major, desired.minor)
}

fn process_debcargo(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(false)?;

    let cratename = match context
        .metadata
        .get("Cargo-Crate")
        .and_then(|v| v.datum.as_str())
    {
        Some(cratename) => cratename.to_string(),
        None => context.metadata.name().unwrap().replace("_", "-"),
    };
    // Only set semver_suffix if this is not the latest version
    use semver::Version as VersionInfo;

    // Extract the base version from Debian-style versions (e.g., "0+git20250809.1.4f78468" -> "0")
    let base_version = crate::names::debian_to_upstream_version(&context.upstream_version);

    // For snapshot versions, try to extract the actual version from Cargo.toml
    let version_to_use = cargo_version_to_use(base_version, context.metadata.version());

    let desired_version = VersionInfo::parse(version_to_use).map_err(|e| {
        Error::Other(format!(
            "Invalid semver version '{}': {}",
            version_to_use, e
        ))
    })?;

    let rt = tokio::runtime::Runtime::new().unwrap();

    let data = rt
        .block_on(upstream_ontologist::providers::rust::load_crate_info(
            &cratename,
        ))
        .map_err(|e| {
            Error::MissingUpstreamInfo(format!(
                "Unable to load crate info for {}: {}",
                cratename, e
            ))
        })?
        .ok_or(Error::MissingUpstreamInfo(format!(
            "crates.io has no crate {}",
            cratename
        )))?;
    let mut features = None;
    let mut crate_version = None;
    let mut semver_suffix = false;
    for version_info in data.versions {
        let available_version = &version_info.num;
        if crate_version_is_newer(available_version, &desired_version) {
            semver_suffix = true;
            break;
        }
        if VersionInfo::parse(&debian_analyzer::debcargo::unmangle_debcargo_version(
            &context.upstream_version,
        ))
        .unwrap()
            == version_info.num
        {
            crate_version = Some(version_info.num);
            features = Some(version_info.features.clone());
        }
    }
    let mut control = debian_analyzer::debcargo::DebcargoEditor::new();
    control.cargo = Some(toml_edit::DocumentMut::new());
    control.cargo.as_mut().unwrap()["package"]["name"] = toml_edit::value(cratename);
    if let Some(crate_version) = crate_version {
        control.cargo.as_mut().unwrap()["package"]["version"] =
            toml_edit::value(crate_version.to_string());
    }
    if let Some(features) = features {
        populate_cargo_features(control.cargo.as_mut().unwrap(), &features);
    }
    control.debcargo["semver_suffix"] = toml_edit::value(semver_suffix);
    control.debcargo["overlay"] = toml_edit::value(".");
    control.commit()?;
    Ok(())
}

fn process_cargo(context: &mut ProcessorContext) -> Result<(), Error> {
    context.kickstart_tree(true)?;

    let mut control = context.create_control_file()?;
    let upstream_name = match context.metadata.name() {
        Some(name) => name,
        None => {
            return Err(Error::MissingUpstreamInfo(
                "unable to determine the name from cargo.toml".to_string(),
            ))
        }
    };
    let mut source = control
        .add_source(&upstream_package_to_debian_source_name("rust", &upstream_name).unwrap());

    context.bootstrap_debhelper(
        &mut source,
        DebhelperConfig {
            addons: vec!["rust"],
            ..Default::default()
        },
    )?;
    if let Some(ref maintainer) = context.maintainer {
        source.set_maintainer(maintainer);
    }
    if let Some(url) = context.metadata.repository_browse() {
        source.set_vcs_browser(Some(url));
    };
    if let Some(url) = context.metadata.repository() {
        source.set_vcs_git(url);
    };
    source.set_section(Some("rust"));
    let (build_deps, _test_deps) = context.get_project_wide_deps();
    import_build_deps(&mut source, &build_deps);
    source.set_standards_version(&latest_standards_version().to_string());

    if let Some(homepage_str) = context.metadata.homepage() {
        match Url::parse(homepage_str) {
            Ok(url) => source.set_homepage(&url),
            Err(_) => {} // ignoring the error if homepage url is not found
        }
    }

    control.commit()?;
    Ok(())
}

type Processor = fn(&mut ProcessorContext) -> Result<(), Error>;

/// Select the processor for an ognibuild buildsystem name.
fn select_processor(buildsystem_name: &str, use_debcargo: bool) -> Processor {
    match buildsystem_name {
        "setup.py" => process_setup_py,
        "node" => process_npm,
        "maven" | "gradle" => process_maven,
        "Dist::Zilla" => process_dist_zilla,
        "meson" => process_meson,
        "Module::Build::Tiny" => process_perl_build_tiny,
        "cargo" if use_debcargo => process_debcargo, // if debcargo.toml needs to be generated
        "cargo" => process_cargo,
        "golang" => process_golang,
        "R" => process_r,
        "octave" => process_octave,
        "cmake" => process_cmake,
        "make" => process_make, // Handles autotools too
        _ => process_default,
    }
}

pub fn process(
    session: &dyn Session,
    wt: &dyn PyWorkingTree,
    subpath: PathBuf,
    debian_path: PathBuf,
    upstream_version: String,
    metadata: &UpstreamMetadata,
    compat_release: String,
    buildsystem: Box<dyn BuildSystem>,
    buildsystem_subpath: PathBuf,
    maintainer: Option<String>,
    _kickstart_from_dist: Option<Box<dyn FnOnce(&dyn PyWorkingTree, &Path) -> Result<(), Error>>>,
    use_debcargo: bool,
) -> Result<(), Error> {
    let processor = select_processor(buildsystem.name(), use_debcargo);
    let mut context = ProcessorContext {
        session,
        wt,
        subpath,
        debian_path,
        upstream_version,
        metadata,
        compat_release,
        buildsystem,
        _buildsystem_subpath: buildsystem_subpath,
        maintainer,
        _kickstart_from_dist,
    };
    processor(&mut context)
}

/// Check if a Python project supports Python 3
fn check_python3_support(wt: &dyn PyWorkingTree, subpath: &Path) -> Result<bool, Error> {
    // Check setup.py for Python 3 classifiers or version requirements
    let setup_py_path = subpath.join("setup.py");
    if wt.has_filename(&setup_py_path) {
        match wt.get_file_text(&setup_py_path) {
            Ok(content) => {
                let content_str = String::from_utf8_lossy(&content);
                // Look for Python 3 classifiers
                if content_str.contains("Programming Language :: Python :: 3")
                    || content_str.contains("python_requires") && content_str.contains(">=3")
                {
                    return Ok(true);
                }
                // Look for version requirements that include Python 3
                if content_str.contains("python_requires")
                    && (content_str.contains(">=2.7") || content_str.contains(">=2.6"))
                {
                    return Ok(true); // Likely supports both 2 and 3
                }
            }
            Err(_) => {} // Ignore file read errors
        }
    }

    // Check pyproject.toml
    let pyproject_path = subpath.join("pyproject.toml");
    if wt.has_filename(&pyproject_path) {
        match wt.get_file_text(&pyproject_path) {
            Ok(content) => {
                let content_str = String::from_utf8_lossy(&content);
                if content_str.contains("python") && content_str.contains(">=3") {
                    return Ok(true);
                }
            }
            Err(_) => {} // Ignore file read errors
        }
    }

    // Check for tox.ini with py3* environments
    let tox_path = subpath.join("tox.ini");
    if wt.has_filename(&tox_path) {
        match wt.get_file_text(&tox_path) {
            Ok(content) => {
                let content_str = String::from_utf8_lossy(&content);
                if content_str.contains("py3") || content_str.contains("python3") {
                    return Ok(true);
                }
            }
            Err(_) => {} // Ignore file read errors
        }
    }

    // Default to true for modern Python projects (conservative approach)
    // Most projects created in recent years support Python 3
    Ok(true)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    #[test]
    #[allow(unpredictable_function_pointer_comparisons)]
    fn test_select_processor() {
        // Both ognibuild java buildsystems route to the maven processor.
        assert!(select_processor("maven", false) == process_maven as Processor);
        assert!(select_processor("gradle", false) == process_maven as Processor);
        assert!(select_processor("setup.py", false) == process_setup_py as Processor);
        assert!(select_processor("node", false) == process_npm as Processor);
        assert!(select_processor("cargo", false) == process_cargo as Processor);
        assert!(select_processor("cargo", true) == process_debcargo as Processor);
        assert!(select_processor("bazel", false) == process_default as Processor);
    }

    #[test]
    fn test_debhelper_rules() {
        let mut output = Vec::new();

        // Simple case
        debhelper_rules(&mut output, None, None, HashMap::new()).unwrap();
        let rules_content = String::from_utf8(output).unwrap();
        assert!(rules_content.contains("#!/usr/bin/make -f"));
        assert!(rules_content.contains("%:\n\tdh $@\n"));
        assert!(!rules_content.contains("--buildsystem="));

        // With buildsystem
        let mut output = Vec::new();
        debhelper_rules(&mut output, Some("python"), None, HashMap::new()).unwrap();
        let rules_content = String::from_utf8(output).unwrap();
        assert!(rules_content.contains("dh $@ --buildsystem=python"));

        // With environment variables
        let mut output = Vec::new();
        let mut env = HashMap::new();
        env.insert("TEST_VAR", "test-value");
        env.insert("ANOTHER_VAR", "another-value");
        debhelper_rules(&mut output, None, None, env).unwrap();
        let rules_content = String::from_utf8(output).unwrap();
        assert!(rules_content.contains("export TEST_VAR=test-value"));
        assert!(rules_content.contains("export ANOTHER_VAR=another-value"));
    }

    #[test]
    fn test_enable_dh_addon() {
        // Create a test source with no build dependencies
        let mut control = debian_control::lossless::Control::new();
        let mut source = control.add_source("test-package");

        // Enable an addon
        enable_dh_addon(&mut source, "python3");

        // Verify build dependency string contains the addon
        let build_deps = source.build_depends().unwrap();
        let deps_string = build_deps.to_string();
        assert!(deps_string.contains("dh-sequence-python3"));

        // Enable another addon
        enable_dh_addon(&mut source, "nodejs");

        // Verify both addons are in the dependencies string
        let build_deps = source.build_depends().unwrap();
        let deps_string = build_deps.to_string();
        assert!(deps_string.contains("dh-sequence-python3"));
        assert!(deps_string.contains("dh-sequence-nodejs"));
    }

    #[test]
    fn test_import_build_deps() {
        // Create a test source with initial build dependencies
        let mut control = debian_control::lossless::Control::new();
        let mut source = control.add_source("test-package");
        source.set_build_depends(&"debhelper-compat (= 13)".parse().unwrap());

        // Create test Relations with parsed Relations string
        let new_build_deps: debian_control::lossless::relations::Relations =
            "python3-all, dh-python".parse().unwrap();

        // Import the build dependencies
        import_build_deps(&mut source, &new_build_deps);

        // Verify all dependencies are present using string matching
        let build_deps = source.build_depends().unwrap();
        let deps_string = build_deps.to_string();
        assert!(deps_string.contains("debhelper-compat (= 13)"));
        assert!(deps_string.contains("python3-all"));
        assert!(deps_string.contains("dh-python"));

        // Add a dependency with version
        let versioned_deps: debian_control::lossless::relations::Relations =
            "python3-all (>= 3.9)".parse().unwrap();

        // Import the versioned dependency
        import_build_deps(&mut source, &versioned_deps);

        // Verify the versioned dependency is now present
        let build_deps = source.build_depends().unwrap();
        let deps_string = build_deps.to_string();
        assert!(deps_string.contains("python3-all (>= 3.9)"));
    }

    #[test]
    fn test_debhelper_config() {
        // Test the DebhelperConfig struct and defaults
        let config = DebhelperConfig {
            addons: vec!["python3", "nodejs"],
            env: {
                let mut env = HashMap::new();
                env.insert("TEST_VAR", "test-value");
                env
            },
            buildsystem: Some("pybuild"),
            build_directory: None,
        };

        assert_eq!(config.addons.len(), 2);
        assert_eq!(config.addons[0], "python3");
        assert_eq!(config.addons[1], "nodejs");
        assert_eq!(config.env.get("TEST_VAR"), Some(&"test-value"));
        assert_eq!(config.buildsystem, Some("pybuild"));

        // Test default implementation
        let default_config = DebhelperConfig::default();
        assert!(default_config.addons.is_empty());
        assert!(default_config.env.is_empty());
        assert_eq!(default_config.buildsystem, None);
        assert_eq!(default_config.build_directory, None);
    }

    #[test]
    fn test_populate_cargo_features() {
        let mut cargo = toml_edit::DocumentMut::new();
        cargo["features"] = toml_edit::Item::Table(toml_edit::Table::new());

        let mut features = HashMap::new();
        features.insert(
            "default".to_string(),
            vec!["std".to_string(), "alloc".to_string()],
        );
        features.insert("std".to_string(), vec![]);

        populate_cargo_features(&mut cargo, &features);

        let features_section = cargo["features"].as_table().unwrap();
        let default = features_section["default"].as_array().unwrap();
        let default_values: Vec<&str> = default.iter().map(|v| v.as_str().unwrap()).collect();
        assert_eq!(default_values, vec!["std", "alloc"]);

        let std = features_section["std"].as_array().unwrap();
        assert_eq!(std.len(), 0);
    }

    #[test]
    fn test_populate_cargo_features_missing_section_is_noop() {
        // `as_table_mut` on an absent key returns None without inserting, so a
        // document without a `features` section is left without one.
        let mut cargo = toml_edit::DocumentMut::new();

        let mut features = HashMap::new();
        features.insert("foo".to_string(), vec!["bar".to_string()]);

        populate_cargo_features(&mut cargo, &features);

        assert!(cargo.get("features").is_none());
    }

    #[test]
    fn test_populate_cargo_features_non_table_left_untouched() {
        // If the `features` entry is not a table, it must be left untouched
        // rather than panicking.
        let mut cargo = toml_edit::DocumentMut::new();
        cargo["features"] = toml_edit::value("not-a-table");

        let mut features = HashMap::new();
        features.insert("foo".to_string(), vec!["bar".to_string()]);

        populate_cargo_features(&mut cargo, &features);

        assert_eq!(cargo["features"].as_str(), Some("not-a-table"));
    }

    fn init_working_tree(path: &Path) -> breezyshim::workingtree::GenericWorkingTree {
        use breezyshim::controldir::ControlDirFormat;
        breezyshim::init();
        let format = ControlDirFormat::default();
        let transport =
            breezyshim::transport::get_transport(&url::Url::from_file_path(path).unwrap(), None)
                .unwrap();
        let controldir = format.initialize_on_transport(&transport).unwrap();
        controldir.create_repository(None).unwrap();
        controldir.create_branch(None).unwrap();
        controldir.create_workingtree().unwrap()
    }

    #[test]
    fn test_prepare_empty_subpath_creates_versioned_dir() {
        use breezyshim::tree::Tree;
        let td = tempfile::tempdir().unwrap();
        let wt = init_working_tree(td.path());

        let subpath = Path::new("debian");
        assert!(!wt.has_filename(subpath));

        prepare_empty_subpath(&wt, subpath).unwrap();

        assert!(wt.has_filename(subpath));
        assert!(wt.is_versioned(subpath));
    }

    #[test]
    fn test_prepare_empty_subpath_existing_dir_left_versioned() {
        use breezyshim::tree::{MutableTree, Tree};
        let td = tempfile::tempdir().unwrap();
        let wt = init_working_tree(td.path());

        // A pre-existing, already-versioned subpath must remain versioned and
        // must not cause an error.
        let subpath = Path::new("debian");
        wt.mkdir(subpath).unwrap();
        assert!(wt.has_filename(subpath));

        prepare_empty_subpath(&wt, subpath).unwrap();

        assert!(wt.has_filename(subpath));
        assert!(wt.is_versioned(subpath));
    }

    #[test]
    fn test_check_python3_support_classifier() {
        use breezyshim::tree::MutableTree;
        let td = tempfile::tempdir().unwrap();
        let wt = init_working_tree(td.path());

        wt.put_file_bytes_non_atomic(
            Path::new("setup.py"),
            b"setup(classifiers=['Programming Language :: Python :: 3'])",
        )
        .unwrap();

        assert!(check_python3_support(&wt, Path::new("")).unwrap());
    }

    #[test]
    fn test_check_python3_support_no_setup_py_defaults_true() {
        let td = tempfile::tempdir().unwrap();
        let wt = init_working_tree(td.path());

        // With no packaging files present, the conservative default is true.
        assert!(check_python3_support(&wt, Path::new("")).unwrap());
    }

    #[test]
    fn test_check_python3_support_pyproject() {
        use breezyshim::tree::MutableTree;
        let td = tempfile::tempdir().unwrap();
        let wt = init_working_tree(td.path());

        wt.put_file_bytes_non_atomic(
            Path::new("pyproject.toml"),
            b"[project]\nrequires-python = \">=3.8\"\n",
        )
        .unwrap();

        assert!(check_python3_support(&wt, Path::new("")).unwrap());
    }

    #[test]
    fn test_cargo_version_to_use_release() {
        // A real release version is used verbatim, regardless of metadata.
        assert_eq!(cargo_version_to_use("1.2.3", Some("9.9.9")), "1.2.3");
        assert_eq!(cargo_version_to_use("1.2.3", None), "1.2.3");
    }

    #[test]
    fn test_cargo_version_to_use_zero_snapshot() {
        // A "0" base version is a snapshot: prefer the metadata version.
        assert_eq!(cargo_version_to_use("0", Some("4.5.6")), "4.5.6");
        assert_eq!(cargo_version_to_use("0", None), "0.0.0");
    }

    #[test]
    fn test_cargo_version_to_use_git_snapshot() {
        // A version containing "git" is a snapshot: prefer the metadata version.
        assert_eq!(
            cargo_version_to_use("0+git20250809.4f78468", Some("2.0.0")),
            "2.0.0"
        );
        assert_eq!(cargo_version_to_use("0+git20250809.4f78468", None), "0.0.0");
    }

    #[test]
    fn test_crate_version_is_newer() {
        use semver::Version;
        let desired = Version::new(1, 2, 0);

        // Higher major or minor is newer.
        assert!(crate_version_is_newer(&Version::new(2, 0, 0), &desired));
        assert!(crate_version_is_newer(&Version::new(1, 3, 0), &desired));

        // Same major/minor is not newer, even with a higher patch.
        assert!(!crate_version_is_newer(&Version::new(1, 2, 0), &desired));
        assert!(!crate_version_is_newer(&Version::new(1, 2, 9), &desired));

        // Lower major or minor is not newer.
        assert!(!crate_version_is_newer(&Version::new(0, 9, 0), &desired));
        assert!(!crate_version_is_newer(&Version::new(1, 1, 9), &desired));
    }

    #[test]
    fn test_check_python3_support_tox() {
        use breezyshim::tree::MutableTree;
        let td = tempfile::tempdir().unwrap();
        let wt = init_working_tree(td.path());

        wt.put_file_bytes_non_atomic(Path::new("tox.ini"), b"[tox]\nenvlist = py39,py310\n")
            .unwrap();

        assert!(check_python3_support(&wt, Path::new("")).unwrap());
    }
}
