use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fmt::Write;
use std::fs;
use std::process::Command;

use crate::{Lint, LintExtractor};

/// Descriptions of rustc lint groups.
static GROUP_DESCRIPTIONS: &[(&str, &str)] = &[
    ("unused", "Lints that detect things being declared but not used, or excess syntax"),
    ("let-underscore", "Lints that detect wildcard let bindings that are likely to be invalid"),
    ("rustdoc", "Rustdoc-specific lints"),
    ("rust-2018-idioms", "Lints to nudge you toward idiomatic features of Rust 2018"),
    ("nonstandard-style", "Violation of standard naming conventions"),
    ("future-incompatible", "Lints that detect code that has future-compatibility problems"),
    ("rust-2018-compatibility", "Lints used to transition code from the 2015 edition to 2018"),
    ("rust-2021-compatibility", "Lints used to transition code from the 2018 edition to 2021"),
    ("rust-2024-compatibility", "Lints used to transition code from the 2021 edition to 2024"),
    (
        "refining-impl-trait",
        "Detects refinement of `impl Trait` return types by trait implementations",
    ),
    (
        "keyword-idents",
        "Lints that detect identifiers which will be come keywords in later editions",
    ),
    ("deprecated-safe", "Lints for functions which were erroneously marked as safe in the past"),
    (
        "unknown-or-malformed-diagnostic-attributes",
        "detects unknown or malformed diagnostic attributes",
    ),
];

type LintGroups = BTreeMap<String, BTreeSet<String>>;

impl<'a> LintExtractor<'a> {
    /// Updates the documentation of lint groups.
    pub(crate) fn generate_group_docs(&self, lints: &[Lint]) -> Result<(), Box<dyn Error>> {
        let groups = self.collect_groups()?;
        let groups_path = self.out_path.join("groups.md");
        let contents = fs::read_to_string(&groups_path)
            .map_err(|e| format!("could not read {}: {}", groups_path.display(), e))?;
        let new_contents =
            contents.replace("{{groups-table}}", &self.make_groups_table(lints, &groups)?);
        // Delete the output because bootstrap uses hard links in its copies.
        let _ = fs::remove_file(&groups_path);
        fs::write(&groups_path, new_contents)
            .map_err(|e| format!("could not write to {}: {}", groups_path.display(), e))?;
        Ok(())
    }

    /// Collects the group names from rustc.
    fn collect_groups(&self) -> Result<LintGroups, Box<dyn Error>> {
        let mut result = BTreeMap::new();
        let mut cmd = Command::new(self.rustc_path);
        cmd.arg("-Whelp");
        let output = cmd.output().map_err(|e| format!("failed to run command {:?}\n{}", cmd, e))?;
        if !output.status.success() {
            return Err(format!(
                "failed to collect lint info: failed to run {cmd:?}: {:?}\n--- stderr\n{}--- stdout\n{}\n",
                output.status,
                std::str::from_utf8(&output.stderr).unwrap(),
                std::str::from_utf8(&output.stdout).unwrap(),
            )
            .into());
        }
        let stdout = std::str::from_utf8(&output.stdout).unwrap();
        let lines = stdout.lines();
        let group_start = lines.skip_while(|line| !line.contains("groups provided")).skip(1);
        let table_start = group_start.skip_while(|line| !line.contains("----")).skip(1);
        for line in table_start {
            if line.is_empty() {
                break;
            }
            let mut parts = line.trim().splitn(2, ' ');
            let name = parts.next().expect("name in group");
            if name == "warnings" {
                // This is special.
                continue;
            }
            let lints = parts
                .next()
                .ok_or_else(|| format!("expected lints following name, got `{}`", line))?;
            let lints = lints.split(',').map(|l| l.trim().to_string()).collect();
            assert!(result.insert(name.to_string(), lints).is_none());
        }
        if result.is_empty() {
            return Err(
                format!("expected at least one group in -Whelp output, got:\n{}", stdout).into()
            );
        }
        Ok(result)
    }

    fn make_groups_table(
        &self,
        lints: &[Lint],
        groups: &LintGroups,
    ) -> Result<String, Box<dyn Error>> {
        let mut result = String::new();
        let mut to_link = Vec::new();
        result.push_str("| Group | Description | Lints |\n");
        result.push_str("|-------|-------------|-------|\n");
        result.push_str("| warnings | All lints that are set to issue warnings | See [warn-by-default] for the default set of warnings |\n");
        for (group_name, group_lints) in groups {
            let description = match GROUP_DESCRIPTIONS.iter().find(|(n, _)| n == group_name) {
                Some((_, desc)) => desc,
                None if self.validate => {
                    return Err(format!(
                        "lint group `{}` does not have a description, \
                         please update the GROUP_DESCRIPTIONS list in \
                         src/tools/lint-docs/src/groups.rs",
                        group_name
                    )
                    .into());
                }
                None => {
                    eprintln!(
                        "warning: lint group `{}` is missing from the GROUP_DESCRIPTIONS list\n\
                         If this is a new lint group, please update the GROUP_DESCRIPTIONS in \
                         src/tools/lint-docs/src/groups.rs",
                        group_name
                    );
                    continue;
                }
            };
            to_link.extend(group_lints);
            let brackets: Vec<_> = group_lints.iter().map(|l| format!("[{}]", l)).collect();
            writeln!(result, "| {} | {} | {} |", group_name, description, brackets.join(", "))
                .unwrap();
        }
        result.push('\n');
        result.push_str("[warn-by-default]: listing/warn-by-default.md\n");
        for lint_name in to_link {
            let lint_def = match lints.iter().find(|l| l.name == lint_name.replace('-', "_")) {
                Some(def) => def,
                None => {
                    let msg = format!(
                        "`rustc -W help` defined lint `{}` but that lint does not \
                        appear to exist\n\
                        Check that the lint definition includes the appropriate doc comments.",
                        lint_name
                    );
                    if self.validate {
                        return Err(msg.into());
                    } else {
                        eprintln!("warning: {}", msg);
                        continue;
                    }
                }
            };
            writeln!(
                result,
                "[{}]: listing/{}#{}",
                lint_name,
                lint_def.level.doc_filename(),
                lint_name
            )
            .unwrap();
        }
        Ok(result)
    }
}
