use crate::update_lints::{DeprecatedLint, Lint, find_lint_decls, generate_lint_files, read_deprecated_lints};
use crate::utils::{UpdateMode, Version};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::{fs, io};

/// Runs the `deprecate` command
///
/// This does the following:
/// * Adds an entry to `deprecated_lints.rs`.
/// * Removes the lint declaration (and the entire file if applicable)
///
/// # Panics
///
/// If a file path could not read from or written to
pub fn deprecate(clippy_version: Version, name: &str, reason: &str) {
    if let Some((prefix, _)) = name.split_once("::") {
        panic!("`{name}` should not contain the `{prefix}` prefix");
    }

    let mut lints = find_lint_decls();
    let (mut deprecated_lints, renamed_lints) = read_deprecated_lints();

    let Some(lint) = lints.iter().find(|l| l.name == name) else {
        eprintln!("error: failed to find lint `{name}`");
        return;
    };

    let prefixed_name = String::from_iter(["clippy::", name]);
    match deprecated_lints.binary_search_by(|x| x.name.cmp(&prefixed_name)) {
        Ok(_) => {
            println!("`{name}` is already deprecated");
            return;
        },
        Err(idx) => deprecated_lints.insert(
            idx,
            DeprecatedLint {
                name: prefixed_name,
                reason: reason.into(),
                version: clippy_version.rust_display().to_string(),
            },
        ),
    }

    let mod_path = {
        let mut mod_path = PathBuf::from(format!("clippy_lints/src/{}", lint.module));
        if mod_path.is_dir() {
            mod_path = mod_path.join("mod");
        }

        mod_path.set_extension("rs");
        mod_path
    };

    if remove_lint_declaration(name, &mod_path, &mut lints).unwrap_or(false) {
        generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
        println!("info: `{name}` has successfully been deprecated");
        println!("note: you must run `cargo uitest` to update the test results");
    } else {
        eprintln!("error: lint not found");
    }
}

fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec<Lint>) -> io::Result<bool> {
    fn remove_lint(name: &str, lints: &mut Vec<Lint>) {
        lints.iter().position(|l| l.name == name).map(|pos| lints.remove(pos));
    }

    fn remove_test_assets(name: &str) {
        let test_file_stem = format!("tests/ui/{name}");
        let path = Path::new(&test_file_stem);

        // Some lints have their own directories, delete them
        if path.is_dir() {
            let _ = fs::remove_dir_all(path);
            return;
        }

        // Remove all related test files
        let _ = fs::remove_file(path.with_extension("rs"));
        let _ = fs::remove_file(path.with_extension("stderr"));
        let _ = fs::remove_file(path.with_extension("fixed"));
    }

    fn remove_impl_lint_pass(lint_name_upper: &str, content: &mut String) {
        let impl_lint_pass_start = content.find("impl_lint_pass!").unwrap_or_else(|| {
            content
                .find("declare_lint_pass!")
                .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`"))
        });
        let mut impl_lint_pass_end = content[impl_lint_pass_start..]
            .find(']')
            .expect("failed to find `impl_lint_pass` terminator");

        impl_lint_pass_end += impl_lint_pass_start;
        if let Some(lint_name_pos) = content[impl_lint_pass_start..impl_lint_pass_end].find(lint_name_upper) {
            let mut lint_name_end = impl_lint_pass_start + (lint_name_pos + lint_name_upper.len());
            for c in content[lint_name_end..impl_lint_pass_end].chars() {
                // Remove trailing whitespace
                if c == ',' || c.is_whitespace() {
                    lint_name_end += 1;
                } else {
                    break;
                }
            }

            content.replace_range(impl_lint_pass_start + lint_name_pos..lint_name_end, "");
        }
    }

    if path.exists()
        && let Some(lint) = lints.iter().find(|l| l.name == name)
    {
        if lint.module == name {
            // The lint name is the same as the file, we can just delete the entire file
            fs::remove_file(path)?;
        } else {
            // We can't delete the entire file, just remove the declaration

            if let Some(Some("mod.rs")) = path.file_name().map(OsStr::to_str) {
                // Remove clippy_lints/src/some_mod/some_lint.rs
                let mut lint_mod_path = path.to_path_buf();
                lint_mod_path.set_file_name(name);
                lint_mod_path.set_extension("rs");

                let _ = fs::remove_file(lint_mod_path);
            }

            let mut content =
                fs::read_to_string(path).unwrap_or_else(|_| panic!("failed to read `{}`", path.to_string_lossy()));

            eprintln!(
                "warn: you will have to manually remove any code related to `{name}` from `{}`",
                path.display()
            );

            assert!(
                content[lint.declaration_range.clone()].contains(&name.to_uppercase()),
                "error: `{}` does not contain lint `{}`'s declaration",
                path.display(),
                lint.name
            );

            // Remove lint declaration (declare_clippy_lint!)
            content.replace_range(lint.declaration_range.clone(), "");

            // Remove the module declaration (mod xyz;)
            let mod_decl = format!("\nmod {name};");
            content = content.replacen(&mod_decl, "", 1);

            remove_impl_lint_pass(&lint.name.to_uppercase(), &mut content);
            fs::write(path, content).unwrap_or_else(|_| panic!("failed to write to `{}`", path.to_string_lossy()));
        }

        remove_test_assets(name);
        remove_lint(name, lints);
        return Ok(true);
    }

    Ok(false)
}
