use std::cmp::min;

use rustc_data_structures::fx::FxIndexMap;
use rustc_data_structures::sorted_map::SortedMap;
use rustc_errors::{Diag, DiagLocation, Diagnostic, MultiSpan};
use rustc_hir::{HirId, ItemLocalId};
use rustc_lint_defs::EditionFcw;
use rustc_macros::{Decodable, Encodable, StableHash};
use rustc_session::Session;
use rustc_session::lint::{
    FutureIncompatibilityReason, Level, Lint, LintExpectationId, LintId, StableLintExpectationId,
    UnstableLintExpectationId, builtin,
};
use rustc_span::{DUMMY_SP, ExpnKind, Span, Symbol, kw};
use tracing::instrument;

use crate::ty::TyCtxt;

/// How a lint level was set.
#[derive(Clone, Copy, PartialEq, Eq, Encodable, Decodable, StableHash, Debug)]
pub enum LintLevelSource {
    /// Lint is at the default level as declared in rustc.
    Default,

    /// Lint level was set by an attribute.
    Node {
        name: Symbol,
        span: Span,
        /// RFC 2383 reason
        reason: Option<Symbol>,
    },

    /// Lint level was set by a command-line flag.
    /// The provided `Level` is the level specified on the command line.
    /// (The actual level may be lower due to `--cap-lints`.)
    CommandLine(Symbol, Level),
}

impl LintLevelSource {
    pub fn name(&self) -> Symbol {
        match *self {
            LintLevelSource::Default => kw::Default,
            LintLevelSource::Node { name, .. } => name,
            LintLevelSource::CommandLine(name, _) => name,
        }
    }

    pub fn span(&self) -> Span {
        match *self {
            LintLevelSource::Default => DUMMY_SP,
            LintLevelSource::Node { span, .. } => span,
            LintLevelSource::CommandLine(_, _) => DUMMY_SP,
        }
    }
}

/// Convenience helper for things that are frequently used together.
#[derive(Copy, Clone, Debug, StableHash, Encodable, Decodable)]
pub struct LevelSpec<Id = LintExpectationId> {
    // This field *must* be private. It must be set in tandem with `lint_id`, only in
    // `LevelSpec::new`, because only certain `level`/`lint_id` combinations are valid. See
    // `LevelSpec::new` for those combinations.
    //
    // If you are thinking right now that `level` and `lint_id` should be combined into a single
    // type that excludes the invalid combinations, that's a reasonable thought, but in practice
    // it's painful because `level` needs to be used by itself, without `lint_id`, in many places.
    // Making the fields private prevents invalid combinations while retaining the flexibility of
    // two separate fields.
    level: Level,

    // This field *must* be private. See the comment on `level`.
    lint_id: Option<Id>,

    pub src: LintLevelSource,
}

pub type UnstableLevelSpec = LevelSpec<UnstableLintExpectationId>;
pub type StableLevelSpec = LevelSpec<StableLintExpectationId>;

impl<Id: Copy> LevelSpec<Id> {
    // Panics if an invalid `level`/`lint_id` combination is given.
    pub fn new(level: Level, lint_id: Option<Id>, src: LintLevelSource) -> LevelSpec<Id> {
        match (level, lint_id) {
            (Level::Allow | Level::Warn | Level::Deny | Level::Forbid, None) => {}
            (Level::Expect, Some(_)) => {}
            (Level::ForceWarn, _) => {}
            _ => panic!("invalid level/lint_id combination"),
        }
        LevelSpec { level, lint_id, src }
    }

    pub fn level(self) -> Level {
        self.level
    }

    pub fn is_allow(self) -> bool {
        self.level == Level::Allow
    }

    pub fn is_expect(self) -> bool {
        self.level == Level::Expect
    }

    pub fn lint_id(self) -> Option<Id> {
        self.lint_id
    }
}

impl From<UnstableLevelSpec> for LevelSpec {
    fn from(level: UnstableLevelSpec) -> LevelSpec {
        let LevelSpec { level, lint_id, src } = level;
        let lint_id = lint_id.map(LintExpectationId::Unstable);
        LevelSpec { level, lint_id, src }
    }
}

impl From<StableLevelSpec> for LevelSpec {
    fn from(level: StableLevelSpec) -> LevelSpec {
        let LevelSpec { level, lint_id, src } = level;
        let lint_id = lint_id.map(LintExpectationId::Stable);
        LevelSpec { level, lint_id, src }
    }
}

/// Return type for the `shallow_lint_levels_on` query.
///
/// This map represents lints levels given by the attributes for *a single HirId*.
#[derive(Default, Debug, StableHash)]
pub struct ShallowLintLevelMap {
    // All the specs for this HirId. This is accessed frequently, e.g. for every lint emitted.
    pub specs: SortedMap<ItemLocalId, FxIndexMap<LintId, StableLevelSpec>>,

    // Additional information about the `expect` specs for this HirId. This is consulted only once
    // per compilation session, in `check_expectations`/`lint_expectations`.
    pub expectations: Vec<(StableLintExpectationId, LintExpectation)>,
}

/// Verify the effect of special annotations: `warnings` lint level and lint caps.
///
/// The return of this function is suitable for diagnostics.
pub fn reveal_actual_level_spec<Id: Copy>(
    sess: &Session,
    lint: LintId,
    probe_for_lint_level_spec: impl Fn(LintId) -> Option<LevelSpec<Id>>,
) -> LevelSpec<Id> {
    let level_spec = probe_for_lint_level_spec(lint);

    // If `level` is none then we actually assume the default level for this lint.
    let mut level_spec = level_spec.unwrap_or_else(|| {
        LevelSpec::new(lint.lint.default_level(sess.edition()), None, LintLevelSource::Default)
    });

    // If we're about to issue a warning, check at the last minute for any
    // directives against the `warnings` lint group. If, for example, there's an
    // `allow(warnings)` in scope then we want to respect that instead.
    if level_spec.level == Level::Warn {
        if let Some(configured_level_spec) =
            probe_for_lint_level_spec(LintId::of(builtin::WARNINGS))
        {
            let respect_warnings_lint_group = match configured_level_spec.level {
                // -Wwarnings is a no-op.
                Level::Warn => false,
                // Some warnings cannot be denied from the `warnings` lint group, only individually.
                Level::Deny | Level::Forbid => !lint.lint.ignore_deny_warnings,
                // All warnings respect -Awarnings.
                Level::Allow => true,
                // Not sure what the right behavior is here, but, sure, why not.
                // See tests/ui/lint/rfc-2383-lint-reason/expect_warnings.rs.
                Level::Expect => true,
                Level::ForceWarn => {
                    sess.dcx().span_delayed_bug(
                        configured_level_spec.src.span(),
                        "cannot --force-warn the `warnings` lint group",
                    );
                    false
                }
            };
            if respect_warnings_lint_group {
                level_spec = configured_level_spec;
            }
        }
    }

    // Ensure that we never exceed the `--cap-lints` argument unless the source is a --force-warn
    if !matches!(level_spec.src, LintLevelSource::CommandLine(_, Level::ForceWarn)) {
        level_spec.level = min(level_spec.level, sess.opts.lint_cap.unwrap_or(Level::Forbid));
    };

    // Ensure that we never exceed driver level.
    if let Some(driver_level) = sess.driver_lint_caps.get(&lint) {
        level_spec.level = min(level_spec.level, *driver_level);
    }

    level_spec
}

impl ShallowLintLevelMap {
    /// Perform a deep probe in the HIR tree looking for the actual level spec for the lint.
    /// This lint level spec is not usable for diagnostics, it needs to be corrected by
    /// `reveal_actual_level` beforehand.
    #[instrument(level = "trace", skip(self, tcx), ret)]
    fn probe_for_lint_level_spec(
        &self,
        tcx: TyCtxt<'_>,
        id: LintId,
        start: HirId,
    ) -> Option<StableLevelSpec> {
        if let Some(map) = self.specs.get(&start.local_id)
            && let Some(level_spec) = map.get(&id)
        {
            return Some(*level_spec);
        }

        let mut owner = start.owner;
        let mut specs = &self.specs;

        for parent in tcx.hir_parent_id_iter(start) {
            if parent.owner != owner {
                owner = parent.owner;
                specs = &tcx.shallow_lint_levels_on(owner).specs;
            }
            if let Some(map) = specs.get(&parent.local_id)
                && let Some(level_spec) = map.get(&id)
            {
                return Some(*level_spec);
            }
        }

        None
    }

    /// Fetch and return the user-visible lint level spec for the given lint at the given HirId.
    #[instrument(level = "trace", skip(self, tcx), ret)]
    pub fn lint_level_spec_at_node(
        &self,
        tcx: TyCtxt<'_>,
        lint: LintId,
        cur: HirId,
    ) -> StableLevelSpec {
        reveal_actual_level_spec(tcx.sess, lint, |lint| {
            self.probe_for_lint_level_spec(tcx, lint, cur)
        })
    }
}

impl TyCtxt<'_> {
    /// Fetch and return the user-visible lint level spec for the given lint at the given HirId.
    pub fn lint_level_spec_at_node(self, lint: &'static Lint, id: HirId) -> StableLevelSpec {
        self.shallow_lint_levels_on(id.owner).lint_level_spec_at_node(self, LintId::of(lint), id)
    }
}

/// This struct represents a lint expectation and holds all required information
/// to emit the `unfulfilled_lint_expectations` lint if it is unfulfilled after
/// the `LateLintPass` has completed.
#[derive(Clone, Debug, Encodable, Decodable, StableHash)]
pub struct LintExpectation {
    /// The reason for this expectation that can optionally be added as part of
    /// the attribute. It will be displayed as part of the lint message.
    pub reason: Option<Symbol>,
    /// The [`Span`] of the attribute that this expectation originated from.
    pub emission_span: Span,
    /// Lint messages for the `unfulfilled_lint_expectations` lint will be
    /// adjusted to include an additional note. Therefore, we have to track if
    /// the expectation is for the lint.
    pub is_unfulfilled_lint_expectations: bool,
    /// This will hold the name of the tool that this lint belongs to. For
    /// the lint `clippy::some_lint` the tool would be `clippy`, the same
    /// goes for `rustdoc`. This will be `None` for rustc lints
    pub lint_tool: Option<Symbol>,
}

impl LintExpectation {
    pub fn new(
        reason: Option<Symbol>,
        emission_span: Span,
        is_unfulfilled_lint_expectations: bool,
        lint_tool: Option<Symbol>,
    ) -> Self {
        Self { reason, emission_span, is_unfulfilled_lint_expectations, lint_tool }
    }
}

fn explain_lint_level_source(
    sess: &Session,
    lint: &'static Lint,
    level: Level,
    src: LintLevelSource,
    err: &mut Diag<'_, ()>,
) {
    // Find the name of the lint group that contains the given lint.
    // Assumes the lint only belongs to one group.
    let lint_group_name = |lint| {
        let lint_groups_iter = sess.lint_groups_iter();
        let lint_id = LintId::of(lint);
        lint_groups_iter
            .filter(|lint_group| !lint_group.is_externally_loaded)
            .find(|lint_group| {
                lint_group
                    .lints
                    .iter()
                    .find(|lint_group_lint| **lint_group_lint == lint_id)
                    .is_some()
            })
            .map(|lint_group| lint_group.name)
    };
    let name = lint.name_lower();
    if let Level::Allow = level {
        // Do not point at `#[allow(compat_lint)]` as the reason for a compatibility lint
        // triggering. (#121009)
        return;
    }
    match src {
        LintLevelSource::Default => {
            let level_str = level.as_str();
            match lint_group_name(lint) {
                Some(group_name) => {
                    err.note_once(format!("`#[{level_str}({name})]` (part of `#[{level_str}({group_name})]`) on by default"));
                }
                None => {
                    err.note_once(format!("`#[{level_str}({name})]` on by default"));
                }
            }
        }
        LintLevelSource::CommandLine(lint_flag_val, orig_level) => {
            let flag = orig_level.to_cmd_flag();
            let hyphen_case_lint_name = name.replace('_', "-");
            if lint_flag_val.as_str() == name {
                err.note_once(format!(
                    "requested on the command line with `{flag} {hyphen_case_lint_name}`"
                ));
            } else {
                let hyphen_case_flag_val = lint_flag_val.as_str().replace('_', "-");
                err.note_once(format!(
                    "`{flag} {hyphen_case_lint_name}` implied by `{flag} {hyphen_case_flag_val}`"
                ));
                if matches!(orig_level, Level::Warn | Level::Deny) {
                    let help = if name == "dead_code" {
                        format!(
                            "to override `{flag} {hyphen_case_flag_val}` add `#[expect({name})]` or `#[allow({name})]`"
                        )
                    } else {
                        format!(
                            "to override `{flag} {hyphen_case_flag_val}` add `#[allow({name})]`"
                        )
                    };
                    err.help_once(help);
                }
            }
        }
        LintLevelSource::Node { name: lint_attr_name, span, reason, .. } => {
            if let Some(rationale) = reason {
                err.note(rationale.to_string());
            }
            err.span_note_once(span, "the lint level is defined here");
            if lint_attr_name.as_str() != name {
                let level_str = level.as_str();
                err.note_once(format!(
                    "`#[{level_str}({name})]` implied by `#[{level_str}({lint_attr_name})]`"
                ));
            }
        }
    }

    if let Some(warnings_group) = sess
        .opts
        .lint_opts
        .iter()
        .find_map(|(opt, level)| (opt == "warnings").then_some(level))
        .copied()
        && warnings_group >= Level::Deny
        && level < warnings_group
    {
        err.note_once(format!("the `{name}` lint ignores `-D warnings`"));
    }
}

/// The innermost function for emitting lints implementing the [`trait@Diagnostic`] trait.
///
/// If you are looking to implement a lint, look for higher level functions,
/// for example:
///
/// - [`TyCtxt::emit_node_span_lint`]
/// - `LintContext::opt_span_lint`
#[track_caller]
pub fn emit_lint_base<'a, D: Diagnostic<'a, ()> + 'a>(
    sess: &'a Session,
    lint: &'static Lint,
    level_spec: impl Into<LevelSpec>,
    span: Option<MultiSpan>,
    decorate: D,
) {
    // Avoid codegen bloat from monomorphization by immediately doing dyn dispatch of `decorate` to
    // the "real" work.
    #[track_caller]
    fn emit_lint_base_impl<'a>(
        sess: &'a Session,
        lint: &'static Lint,
        level_spec: LevelSpec,
        span: Option<MultiSpan>,
        decorate: Box<
            dyn FnOnce(rustc_errors::DiagCtxtHandle<'a>, rustc_errors::Level) -> Diag<'a, ()> + 'a,
        >,
    ) {
        let LevelSpec { level, lint_id, src } = level_spec;

        // Check for future incompatibility lints and issue a stronger warning.
        let future_incompatible = lint.future_incompatible;

        let has_future_breakage = future_incompatible.map_or(
            // Default allow lints trigger too often for testing.
            sess.opts.unstable_opts.future_incompat_test && lint.default_level != Level::Allow,
            |incompat| incompat.report_in_deps,
        );

        // Convert lint level to error level.
        let err_level = match level {
            Level::Allow => {
                if has_future_breakage {
                    rustc_errors::Level::Allow
                } else {
                    return;
                }
            }
            Level::Expect => {
                // This case is special as we actually allow the lint itself in this context, but
                // we can't return early like in the case for `Level::Allow` because we still
                // need the lint diagnostic to be emitted to `rustc_error::DiagCtxtInner`.
                //
                // We can also not mark the lint expectation as fulfilled here right away, as it
                // can still be cancelled in the decorate function. All of this means that we simply
                // create a `Diag` and continue as we would for warnings.
                rustc_errors::Level::Expect
            }
            Level::ForceWarn => rustc_errors::Level::ForceWarning,
            Level::Warn => rustc_errors::Level::Warning,
            Level::Deny | Level::Forbid => rustc_errors::Level::Error,
        };

        let disable_suggestions = if let Some(ref span) = span
            // If this code originates in a foreign macro, aka something that this crate
            // did not itself author, then it's likely that there's nothing this crate
            // can do about it. We probably want to skip the lint entirely.
            && span.primary_spans().iter().any(|s| s.in_external_macro(sess.source_map()))
        {
            true
        } else {
            false
        };

        if disable_suggestions {
            // If this is a future incompatible that is not an edition fixing lint
            // it'll become a hard error, so we have to emit *something*. Also,
            // if this lint occurs in the expansion of a macro from an external crate,
            // allow individual lints to opt-out from being reported.
            let incompatible = future_incompatible.is_some_and(|f| f.reason.edition().is_none());

            // In rustc, for the find_attr macro, we want to always emit this.
            // This completely circumvents normal lint checking, which usually doesn't happen for macros from other crates.
            // However, we kind of want that when using find_attr from another rustc crate. So we cheat a little.
            let is_in_find_attr = sess.enable_internal_lints()
                && span.as_ref().is_some_and(|span| {
                    span.primary_spans().iter().any(|s| {
                        s.source_callee().is_some_and(|i| {
                            matches!(i.kind, ExpnKind::Macro(_, name) if name.as_str() == "find_attr")
                        })
                    })
                });

            if !incompatible && !lint.report_in_external_macro && !is_in_find_attr {
                // Don't continue further, since we don't want to have
                // `diag_span_note_once` called for a diagnostic that isn't emitted.
                return;
            }
        }
        // Finally, run `decorate`. `decorate` can call `trimmed_path_str` (directly or indirectly),
        // so we need to make sure when we do call `decorate` that the diagnostic is eventually
        // emitted or we'll get a `must_produce_diag` ICE.
        //
        // When is a diagnostic *eventually* emitted? Well, that is determined by 2 factors:
        // 1. If the corresponding `rustc_errors::Level` is beyond warning, i.e. `ForceWarning(_)`
        //    or `Error`, then the diagnostic will be emitted regardless of CLI options.
        // 2. If the corresponding `rustc_errors::Level` is warning, then that can be affected by
        //    `-A warnings` or `--cap-lints=xxx` on the command line. In which case, the diagnostic
        //    will be emitted if `can_emit_warnings` is true.
        let skip = err_level == rustc_errors::Level::Warning && !sess.dcx().can_emit_warnings();

        let mut err: Diag<'_, ()> = if !skip {
            decorate(sess.dcx(), err_level)
        } else {
            Diag::new(sess.dcx(), err_level, "")
        };
        // FIXME: Find a nicer way to expose the `DiagLocation`
        err.emitted_at = DiagLocation::caller();

        if let Some(span) = span
            && err.span.primary_span().is_none()
        {
            // We can't use `err.span()` because it overwrites the labels, so we need to do it manually.
            for primary in span.primary_spans() {
                err.span.push_primary_span(*primary);
            }
            for (label_span, label) in span.span_labels_raw() {
                err.span.push_span_diag(*label_span, label.clone());
            }
        }
        if let Some(lint_id) = lint_id {
            err.lint_id(lint_id);
        }

        if disable_suggestions {
            // Any suggestions made here are likely to be incorrect, so anything we
            // emit shouldn't be automatically fixed by rustfix.
            err.disable_suggestions();
        }

        err.is_lint(lint.name_lower(), has_future_breakage, lint.rust_version);
        // Lint diagnostics that are covered by the expect level will not be emitted outside
        // the compiler. It is therefore not necessary to add any information for the user.
        // This will therefore directly call the decorate function which will in turn emit
        // the diagnostic.
        if let Level::Expect = level {
            err.emit();
            return;
        }

        if let Some(future_incompatible) = future_incompatible {
            let explanation = match future_incompatible.reason {
                FutureIncompatibilityReason::FutureReleaseError(_) => {
                    "this was previously accepted by the compiler but is being phased out; \
                         it will become a hard error in a future release!"
                        .to_owned()
                }
                FutureIncompatibilityReason::FutureReleaseSemanticsChange(_) => {
                    "this will change its meaning in a future release!".to_owned()
                }
                FutureIncompatibilityReason::EditionError(EditionFcw { edition, .. }) => {
                    let current_edition = sess.edition();
                    format!(
                        "this is accepted in the current edition (Rust {current_edition}) but is a hard error in Rust {edition}!"
                    )
                }
                FutureIncompatibilityReason::EditionSemanticsChange(EditionFcw {
                    edition, ..
                }) => {
                    format!("this changes meaning in Rust {edition}")
                }
                FutureIncompatibilityReason::EditionAndFutureReleaseError(EditionFcw {
                    edition,
                    ..
                }) => {
                    format!(
                        "this was previously accepted by the compiler but is being phased out; \
                         it will become a hard error in Rust {edition} and in a future release in all editions!"
                    )
                }
                FutureIncompatibilityReason::EditionAndFutureReleaseSemanticsChange(
                    EditionFcw { edition, .. },
                ) => {
                    format!(
                        "this changes meaning in Rust {edition} and in a future release in all editions!"
                    )
                }
                FutureIncompatibilityReason::Custom(reason, _) => reason.to_owned(),
                FutureIncompatibilityReason::Unreachable => unreachable!(),
            };

            if future_incompatible.explain_reason {
                err.warn(explanation);
            }

            let citation =
                format!("for more information, see {}", future_incompatible.reason.reference());
            err.note(citation);
        }

        explain_lint_level_source(sess, lint, level, src, &mut err);
        err.emit();
    }
    emit_lint_base_impl(
        sess,
        lint,
        level_spec.into(),
        span,
        Box::new(move |dcx, level| decorate.into_diag(dcx, level)),
    );
}
