/*
 * Copyright Cedar Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use std::{
    collections::{HashMap, HashSet},
    fmt::Display,
    iter::{Chain, Once},
    sync::{Arc, LazyLock},
    vec,
};

use crate::{
    ast::AnyId,
    impl_diagnostic_from_source_loc_opt_field, impl_diagnostic_from_two_source_loc_opt_fields,
    parser::{
        err::{expected_to_string, ExpectedTokenConfig},
        unescape::UnescapeError,
        Loc, Node,
    },
};
use lalrpop_util as lalr;

use miette::{Diagnostic, LabeledSpan, SourceSpan};
use nonempty::NonEmpty;
use smol_str::{SmolStr, ToSmolStr};
use thiserror::Error;

use super::ast::PR;

#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum UserError {
    #[error("An empty list was passed")]
    EmptyList(Node<()>),
    #[error("Invalid escape codes")]
    StringEscape(Node<NonEmpty<UnescapeError>>),
    #[error("`{0}` is a reserved identifier")]
    ReservedIdentifierUsed(Node<SmolStr>),
    #[error("duplicate annotations: `{}`", .0)]
    DuplicateAnnotations(AnyId, Node<()>, Node<()>),
}

impl UserError {
    // Extract a primary source span locating the error.
    pub(crate) fn primary_source_span(&self) -> Option<SourceSpan> {
        match self {
            Self::EmptyList(n) => n.loc.as_ref().map(|loc| loc.span),
            Self::StringEscape(n) => n.loc.as_ref().map(|loc| loc.span),
            Self::ReservedIdentifierUsed(n) => n.loc.as_ref().map(|loc| loc.span),
            // use the first occurrence as the primary source span
            Self::DuplicateAnnotations(_, n, _) => n.loc.as_ref().map(|loc| loc.span),
        }
    }
}

pub(crate) type RawLocation = usize;
pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>;

pub(crate) type RawParseError<'a> = lalr::ParseError<RawLocation, RawToken<'a>, UserError>;
pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery<RawLocation, RawToken<'a>, UserError>;

type OwnedRawParseError = lalr::ParseError<RawLocation, String, UserError>;

static SCHEMA_TOKEN_CONFIG: LazyLock<ExpectedTokenConfig> = LazyLock::new(|| ExpectedTokenConfig {
    friendly_token_names: HashMap::from([
        ("IN", "`in`"),
        ("PRINCIPAL", "`principal`"),
        ("ACTION", "`action`"),
        ("RESOURCE", "`resource`"),
        ("CONTEXT", "`context`"),
        ("STRINGLIT", "string literal"),
        ("ENTITY", "`entity`"),
        ("NAMESPACE", "`namespace`"),
        ("TYPE", "`type`"),
        ("SET", "`Set`"),
        ("IDENTIFIER", "identifier"),
        ("TAGS", "`tags`"),
        ("ENUM", "`enum`"),
    ]),
    impossible_tokens: HashSet::new(),
    special_identifier_tokens: HashSet::from([
        "NAMESPACE",
        "ENTITY",
        "IN",
        "TYPE",
        "APPLIESTO",
        "PRINCIPAL",
        "ACTION",
        "RESOURCE",
        "CONTEXT",
        "ATTRIBUTES",
        "TAGS",
        "LONG",
        "STRING",
        "BOOL",
        "ENUM",
    ]),
    identifier_sentinel: "IDENTIFIER",
    first_set_identifier_tokens: HashSet::from(["SET"]),
    first_set_sentinel: "\"{\"",
});

/// For errors during parsing
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseError {
    /// Error generated by lalrpop
    pub(crate) err: OwnedRawParseError,
    /// Source code
    src: Arc<str>,
}

impl ParseError {
    pub(crate) fn from_raw_parse_error(err: RawParseError<'_>, src: Arc<str>) -> Self {
        Self {
            err: err.map_token(|token| token.to_string()),
            src,
        }
    }

    pub(crate) fn from_raw_error_recovery(recovery: RawErrorRecovery<'_>, src: Arc<str>) -> Self {
        Self::from_raw_parse_error(recovery.error, src)
    }
}

impl ParseError {
    /// Extract a primary source span locating the error.
    pub fn primary_source_span(&self) -> Option<SourceSpan> {
        match &self.err {
            OwnedRawParseError::InvalidToken { location } => Some(SourceSpan::from(*location)),
            OwnedRawParseError::UnrecognizedEof { location, .. } => {
                Some(SourceSpan::from(*location))
            }
            OwnedRawParseError::UnrecognizedToken {
                token: (token_start, _, token_end),
                ..
            } => Some(SourceSpan::from(*token_start..*token_end)),
            OwnedRawParseError::ExtraToken {
                token: (token_start, _, token_end),
            } => Some(SourceSpan::from(*token_start..*token_end)),
            OwnedRawParseError::User { error } => error.primary_source_span(),
        }
    }
}

impl Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Self { err, .. } = self;
        match err {
            OwnedRawParseError::InvalidToken { .. } => write!(f, "invalid token"),
            OwnedRawParseError::UnrecognizedEof { .. } => write!(f, "unexpected end of input"),
            OwnedRawParseError::UnrecognizedToken {
                token: (_, token, _),
                ..
            } => write!(f, "unexpected token `{token}`"),
            OwnedRawParseError::ExtraToken {
                token: (_, token, _),
                ..
            } => write!(f, "extra token `{token}`"),
            OwnedRawParseError::User { error } => write!(f, "{error}"),
        }
    }
}

impl std::error::Error for ParseError {}

impl Diagnostic for ParseError {
    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
        Some(&self.src as &dyn miette::SourceCode)
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
        let primary_source_span = self.primary_source_span();
        match &self.err {
            OwnedRawParseError::InvalidToken { .. } => primary_source_span
                .map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
            OwnedRawParseError::UnrecognizedEof { expected, .. } => {
                primary_source_span.map(|span| {
                    Box::new(std::iter::once(LabeledSpan::new_with_span(
                        expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
                        span,
                    ))) as _
                })
            }
            OwnedRawParseError::UnrecognizedToken { expected, .. } => {
                primary_source_span.map(|span| {
                    Box::new(std::iter::once(LabeledSpan::new_with_span(
                        expected_to_string(expected, &SCHEMA_TOKEN_CONFIG),
                        span,
                    ))) as _
                })
            }
            OwnedRawParseError::ExtraToken { .. } => primary_source_span
                .map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
            OwnedRawParseError::User {
                error: UserError::DuplicateAnnotations(_, n1, n2),
            } => {
                let spans: Vec<_> = [&n1.loc, &n2.loc]
                    .into_iter()
                    .filter_map(|opt_loc| opt_loc.as_ref())
                    .map(|loc| LabeledSpan::underline(loc.span))
                    .collect();

                if spans.is_empty() {
                    None
                } else {
                    Some(Box::new(spans.into_iter()))
                }
            }
            OwnedRawParseError::User { .. } => primary_source_span
                .map(|span| Box::new(std::iter::once(LabeledSpan::underline(span))) as _),
        }
    }
}

/// Multiple parse errors.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseErrors(pub(crate) Box<NonEmpty<ParseError>>);

impl ParseErrors {
    pub fn new(first: ParseError, tail: impl IntoIterator<Item = ParseError>) -> Self {
        Self(Box::new(NonEmpty {
            head: first,
            tail: tail.into_iter().collect(),
        }))
    }

    pub fn from_iter(i: impl IntoIterator<Item = ParseError>) -> Option<Self> {
        let v = i.into_iter().collect::<Vec<_>>();
        Some(Self(Box::new(NonEmpty::from_vec(v)?)))
    }

    /// Borrowed Iterator over reported errors
    pub fn iter(&self) -> impl Iterator<Item = &ParseError> {
        self.0.iter()
    }
}

impl Display for ParseErrors {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0.first())
    }
}

impl IntoIterator for ParseErrors {
    type Item = ParseError;
    type IntoIter = Chain<Once<ParseError>, vec::IntoIter<ParseError>>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl std::error::Error for ParseErrors {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        std::error::Error::source(self.0.first())
    }
}

// Except for `.related()`, everything else is forwarded to the first error, if it is present.
// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
// information for the first error, even if they don't realize there are multiple errors here.
// See cedar-policy/cedar#326.
impl Diagnostic for ParseErrors {
    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
        errs.next().map(move |first_err| match first_err.related() {
            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
        })
    }

    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Diagnostic::code(self.0.first())
    }

    fn severity(&self) -> Option<miette::Severity> {
        Diagnostic::severity(self.0.first())
    }

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Diagnostic::help(self.0.first())
    }

    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Diagnostic::url(self.0.first())
    }

    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
        Diagnostic::source_code(self.0.first())
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
        Diagnostic::labels(self.0.first())
    }

    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
        Diagnostic::diagnostic_source(self.0.first())
    }
}

/// Non-empty collection of [`ToJsonSchemaError`]
// WARNING: This type is publicly exported from [`cedar-policy`]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToJsonSchemaErrors(NonEmpty<ToJsonSchemaError>);

impl ToJsonSchemaErrors {
    /// Constructor. Guaranteed to have at least one error by construction.
    pub fn new(errs: NonEmpty<ToJsonSchemaError>) -> Self {
        Self(errs)
    }

    /// (Borrowed) iterator
    pub fn iter(&self) -> impl Iterator<Item = &ToJsonSchemaError> {
        self.0.iter()
    }
}

impl IntoIterator for ToJsonSchemaErrors {
    type Item = ToJsonSchemaError;
    type IntoIter = <NonEmpty<ToJsonSchemaError> as IntoIterator>::IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl From<ToJsonSchemaError> for ToJsonSchemaErrors {
    fn from(value: ToJsonSchemaError) -> Self {
        Self(NonEmpty::singleton(value))
    }
}

impl Display for ToJsonSchemaErrors {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0.first()) // intentionally showing only the first error; see #326 for discussion on a similar error type
    }
}

impl std::error::Error for ToJsonSchemaErrors {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.0.first().source()
    }

    #[allow(deprecated)]
    fn description(&self) -> &str {
        self.0.first().description()
    }

    #[allow(deprecated)]
    fn cause(&self) -> Option<&dyn std::error::Error> {
        self.0.first().cause()
    }
}

// Except for `.related()`, everything else is forwarded to the first error, if it is present.
// This ensures that users who only use `Display`, `.code()`, `.labels()` etc, still get rich
// information for the first error, even if they don't realize there are multiple errors here.
// See #326 for discussion on a similar error type.
impl Diagnostic for ToJsonSchemaErrors {
    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
        // the .related() on the first error, and then the 2nd through Nth errors (but not their own .related())
        let mut errs = self.iter().map(|err| err as &dyn Diagnostic);
        errs.next().map(move |first_err| match first_err.related() {
            Some(first_err_related) => Box::new(first_err_related.chain(errs)),
            None => Box::new(errs) as Box<dyn Iterator<Item = _>>,
        })
    }

    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        self.0.first().code()
    }

    fn severity(&self) -> Option<miette::Severity> {
        self.0.first().severity()
    }

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        self.0.first().help()
    }

    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        self.0.first().url()
    }

    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
        self.0.first().source_code()
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
        self.0.first().labels()
    }

    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
        self.0.first().diagnostic_source()
    }
}

// WARNING: This error type is publicly exported in `cedar-policy`, so it is part of the public interface
/// For errors during schema format conversion
#[derive(Clone, Debug, Error, PartialEq, Eq, Diagnostic)]
pub enum ToJsonSchemaError {
    /// Error raised when there are duplicate declarations
    #[error(transparent)]
    #[diagnostic(transparent)]
    DuplicateDeclarations(#[from] DuplicateDeclarations),
    /// Error raised when an action has multiple context declarations
    #[error(transparent)]
    #[diagnostic(transparent)]
    DuplicateContext(#[from] DuplicateContext),
    /// Error raised when a `principal` or `resource` is declared multiple times
    #[error(transparent)]
    #[diagnostic(transparent)]
    DuplicatePrincipalOrResource(#[from] DuplicatePrincipalOrResource),
    /// Error raised when an action does not define either `principal` or `resource`
    #[error(transparent)]
    #[diagnostic(transparent)]
    NoPrincipalOrResource(#[from] NoPrincipalOrResource),
    /// Error raised when there are duplicate namespace IDs
    #[error(transparent)]
    #[diagnostic(transparent)]
    DuplicateNamespaces(#[from] DuplicateNamespace),
    /// Error raised when a type name is unknown
    #[error(transparent)]
    #[diagnostic(transparent)]
    UnknownTypeName(#[from] UnknownTypeName),
    /// Invalid type name
    #[error(transparent)]
    #[diagnostic(transparent)]
    ReservedName(#[from] ReservedName),
    /// Use reserved schema keywords
    #[error(transparent)]
    #[diagnostic(transparent)]
    ReservedSchemaKeyword(#[from] ReservedSchemaKeyword),
}

impl ToJsonSchemaError {
    pub(crate) fn duplicate_context(
        name: &impl ToSmolStr,
        loc1: Option<Loc>,
        loc2: Option<Loc>,
    ) -> Self {
        Self::DuplicateContext(DuplicateContext {
            name: name.to_smolstr(),
            loc1,
            loc2,
        })
    }

    pub(crate) fn duplicate_decls(
        decl: &impl ToSmolStr,
        loc1: Option<Loc>,
        loc2: Option<Loc>,
    ) -> Self {
        Self::DuplicateDeclarations(DuplicateDeclarations {
            decl: decl.to_smolstr(),
            loc1,
            loc2,
        })
    }

    pub(crate) fn duplicate_namespace(
        namespace_id: &impl ToSmolStr,
        loc1: Option<Loc>,
        loc2: Option<Loc>,
    ) -> Self {
        Self::DuplicateNamespaces(DuplicateNamespace {
            namespace_id: namespace_id.to_smolstr(),
            loc1,
            loc2,
        })
    }

    pub(crate) fn duplicate_principal(
        name: &impl ToSmolStr,
        loc1: Option<Loc>,
        loc2: Option<Loc>,
    ) -> Self {
        Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
            name: name.to_smolstr(),
            kind: PR::Principal,
            loc1,
            loc2,
        })
    }

    pub(crate) fn duplicate_resource(
        name: &impl ToSmolStr,
        loc1: Option<Loc>,
        loc2: Option<Loc>,
    ) -> Self {
        Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource {
            name: name.to_smolstr(),
            kind: PR::Resource,
            loc1,
            loc2,
        })
    }

    pub(crate) fn no_principal(name: &impl ToSmolStr, name_loc: Option<Loc>) -> Self {
        Self::NoPrincipalOrResource(NoPrincipalOrResource {
            kind: PR::Principal,
            name: name.to_smolstr(),
            missing_or_empty: MissingOrEmpty::Missing,
            name_loc,
        })
    }

    pub(crate) fn no_resource(name: &impl ToSmolStr, name_loc: Option<Loc>) -> Self {
        Self::NoPrincipalOrResource(NoPrincipalOrResource {
            kind: PR::Resource,
            name: name.to_smolstr(),
            missing_or_empty: MissingOrEmpty::Missing,
            name_loc,
        })
    }

    pub(crate) fn empty_principal(
        name: &impl ToSmolStr,
        name_loc: Option<Loc>,
        loc: Option<Loc>,
    ) -> Self {
        Self::NoPrincipalOrResource(NoPrincipalOrResource {
            kind: PR::Principal,
            name: name.to_smolstr(),
            missing_or_empty: MissingOrEmpty::Empty { loc },
            name_loc,
        })
    }

    pub(crate) fn empty_resource(
        name: &impl ToSmolStr,
        name_loc: Option<Loc>,
        loc: Option<Loc>,
    ) -> Self {
        Self::NoPrincipalOrResource(NoPrincipalOrResource {
            kind: PR::Resource,
            name: name.to_smolstr(),
            missing_or_empty: MissingOrEmpty::Empty { loc },
            name_loc,
        })
    }

    pub(crate) fn reserved_name(name: &impl ToSmolStr, loc: Option<Loc>) -> Self {
        Self::ReservedName(ReservedName {
            name: name.to_smolstr(),
            loc,
        })
    }

    pub(crate) fn reserved_keyword(keyword: &impl ToSmolStr, loc: Option<Loc>) -> Self {
        Self::ReservedSchemaKeyword(ReservedSchemaKeyword {
            keyword: keyword.to_smolstr(),
            loc,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("this uses a reserved schema keyword: `{keyword}`")]
pub struct ReservedSchemaKeyword {
    keyword: SmolStr,
    loc: Option<Loc>,
}

impl Diagnostic for ReservedSchemaKeyword {
    impl_diagnostic_from_source_loc_opt_field!(loc);

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new("Keywords such as `entity`, `extension`, `set` and `record` cannot be used as common type names"))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("use of the reserved `__cedar` namespace")]
pub struct ReservedName {
    name: SmolStr,
    loc: Option<Loc>,
}

impl Diagnostic for ReservedName {
    impl_diagnostic_from_source_loc_opt_field!(loc);

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new(
            "Names containing `__cedar` (for example: `__cedar::A`, `A::__cedar`, or `A::__cedar::B`) are reserved",
        ))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("unknown type name: `{name}`")]
pub struct UnknownTypeName {
    name: SmolStr,
    loc: Option<Loc>,
}

impl Diagnostic for UnknownTypeName {
    impl_diagnostic_from_source_loc_opt_field!(loc);

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        let msg = format!(
            "Did you mean to define `{}` as an entity type or common type?",
            self.name
        );
        Some(Box::new(msg))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("duplicate `{kind}` declaration in action `{name}`")]
pub struct DuplicatePrincipalOrResource {
    name: SmolStr,
    kind: PR,
    loc1: Option<Loc>,
    loc2: Option<Loc>,
}

impl Diagnostic for DuplicatePrincipalOrResource {
    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        let msg = format!("Actions may only have a single {kind} declaration, but a {kind} declaration may specify a list of entity types like `{kind}: [X, Y, Z]`", kind=self.kind);
        Some(Box::new(msg))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("duplicate context declaration in action `{name}`")]
pub struct DuplicateContext {
    name: SmolStr,
    loc1: Option<Loc>,
    loc2: Option<Loc>,
}

impl Diagnostic for DuplicateContext {
    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new(
            "Try either deleting one of the declarations, or merging into a single declaration",
        ))
    }
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("`{decl}` is declared twice")]
pub struct DuplicateDeclarations {
    decl: SmolStr,
    loc1: Option<Loc>,
    loc2: Option<Loc>,
}

impl Diagnostic for DuplicateDeclarations {
    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
}

#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{}", match .missing_or_empty {
    MissingOrEmpty::Missing => format!("missing `{kind}` declaration for `{name}`"),
    MissingOrEmpty::Empty { .. } => format!("for action `{name}`, `{kind}` is `[]`, which is invalid")
})]
pub struct NoPrincipalOrResource {
    kind: PR,
    name: SmolStr,
    missing_or_empty: MissingOrEmpty,
    /// Loc of the action name
    name_loc: Option<Loc>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum MissingOrEmpty {
    /// The declaration was entirely missing
    Missing,
    /// The declaration was present but defined as `[]`
    Empty {
        /// `Loc` of the declaration
        loc: Option<Loc>,
    },
}

pub const NO_PR_HELP_MSG: &str =
    "Every action must define both `principal` and `resource` targets, and the `principal` and `resource` lists must not be `[]`.";

impl Diagnostic for NoPrincipalOrResource {
    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
        self.name_loc
            .as_ref()
            .map(|l| &l.src as &dyn miette::SourceCode)
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
        match &self.missing_or_empty {
            MissingOrEmpty::Missing => self.name_loc.as_ref().map(|loc| {
                Box::new(std::iter::once(miette::LabeledSpan::underline(loc.span))) as _
            }),
            MissingOrEmpty::Empty { loc } => {
                // also underline the bad declaration
                let action_name = self.name_loc.as_ref().map(|loc| {
                    miette::LabeledSpan::new_with_span(Some("for this action".into()), loc.span)
                });
                let decl = loc.as_ref().map(|loc| {
                    miette::LabeledSpan::new_with_span(Some("must not be `[]`".into()), loc.span)
                });
                let spans: Vec<_> = [action_name, decl].into_iter().flatten().collect();

                if spans.is_empty() {
                    None
                } else {
                    Some(Box::new(spans.into_iter()))
                }
            }
        }
    }

    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
        Some(Box::new(NO_PR_HELP_MSG))
    }
}

#[derive(Debug, Clone, Error, PartialEq, Eq)]
#[error("duplicate namespace id: `{namespace_id}`")]
pub struct DuplicateNamespace {
    namespace_id: SmolStr,
    // `Loc`s are optional here as the implicit empty namespace has no location
    loc1: Option<Loc>,
    loc2: Option<Loc>,
}

impl Diagnostic for DuplicateNamespace {
    impl_diagnostic_from_two_source_loc_opt_fields!(loc1, loc2);
}

/// Error subtypes for [`SchemaWarning`]
pub mod schema_warnings {
    use crate::{impl_diagnostic_from_source_loc_opt_field, parser::Loc};
    use miette::Diagnostic;
    use smol_str::SmolStr;
    use thiserror::Error;

    /// Warning when a builtin Cedar name is shadowed
    //
    // CAUTION: this type is publicly exported in `cedar-policy`.
    // Don't make fields `pub`, don't make breaking changes, and use caution
    // when adding public methods.
    #[derive(Eq, PartialEq, Debug, Clone, Error)]
    #[error("The name `{name}` shadows a builtin Cedar name. You'll have to refer to the builtin as `__cedar::{name}`.")]
    pub struct ShadowsBuiltinWarning {
        pub(crate) name: SmolStr,
        pub(crate) loc: Option<Loc>,
    }

    impl Diagnostic for ShadowsBuiltinWarning {
        impl_diagnostic_from_source_loc_opt_field!(loc);

        fn severity(&self) -> Option<miette::Severity> {
            Some(miette::Severity::Warning)
        }
    }

    /// Warning when an entity name is shadowed by a common type name
    //
    // CAUTION: this type is publicly exported in `cedar-policy`.
    // Don't make fields `pub`, don't make breaking changes, and use caution
    // when adding public methods.
    #[derive(Eq, PartialEq, Debug, Clone, Error)]
    #[error("The common type name {name} shadows an entity name")]
    pub struct ShadowsEntityWarning {
        pub(crate) name: SmolStr,
        pub(crate) entity_loc: Option<Loc>,
        pub(crate) common_loc: Option<Loc>,
    }

    impl Diagnostic for ShadowsEntityWarning {
        fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
            let spans: Vec<_> = [&self.entity_loc, &self.common_loc]
                .into_iter()
                .filter_map(|loc| loc.as_ref())
                .map(miette::LabeledSpan::underline)
                .collect();

            if spans.is_empty() {
                None
            } else {
                Some(Box::new(spans.into_iter()))
            }
        }

        fn source_code(&self) -> Option<&dyn miette::SourceCode> {
            // just have to pick one; we assume `entity_loc` and `common_loc`
            // have the same source code.
            // if that isn't true we'll have a confusing underline.
            self.entity_loc
                .as_ref()
                .map(|loc| &loc.src as &dyn miette::SourceCode)
        }

        fn severity(&self) -> Option<miette::Severity> {
            Some(miette::Severity::Warning)
        }
    }
}

/// Warning when constructing a schema
//
// CAUTION: this type is publicly exported in `cedar-policy`.
// Don't make fields `pub`, don't make breaking changes, and use caution
// when adding public methods.
#[derive(Eq, PartialEq, Debug, Clone, Error, Diagnostic)]
#[non_exhaustive]
pub enum SchemaWarning {
    /// Warning when a declaration shadows a builtin type
    #[error(transparent)]
    #[diagnostic(transparent)]
    ShadowsBuiltin(#[from] schema_warnings::ShadowsBuiltinWarning),
    /// Warning when a declaration shadows an entity type
    #[error(transparent)]
    #[diagnostic(transparent)]
    ShadowsEntity(#[from] schema_warnings::ShadowsEntityWarning),
}
