///Selects a resource of type Project (or any of its children, using the functions on this struct) for lookup or fetch
pub enum Project<'a> {
    /// An error occurred while selecting the resource
    ///
    /// This error will be returned by any lookup/fetch attempts.
    Error(Root<'a>, Error),
    /// We're looking for a resource with the given name in the given
    /// parent collection
    Name(Silo<'a>, &'a Name),
    /// Same as [`Self::Name`], but the name is owned rather than borrowed
    OwnedName(Silo<'a>, Name),
    /// We're looking for a resource with the given primary key
    ///
    /// This has no parent container -- a by-id lookup is always global
    PrimaryKey(Root<'a>, Uuid),
}
impl<'a> Project<'a> {
    /// Fetch the record corresponding to the selected resource
    ///
    /// This is equivalent to `fetch_for(authz::Action::Read)`.
    pub async fn fetch(
        &self,
    ) -> LookupResult<(authz::Silo, authz::Project, nexus_db_model::Project)> {
        self.fetch_for(authz::Action::Read).await
    }
    /// Turn the Result<T, E> of [`fetch`] into a Result<Option<T>, E>.
    pub async fn optional_fetch(
        &self,
    ) -> LookupResult<Option<(authz::Silo, authz::Project, nexus_db_model::Project)>> {
        self.optional_fetch_for(authz::Action::Read).await
    }
    /// Fetch the record corresponding to the selected resource and
    /// check whether the caller is allowed to do the specified `action`
    ///
    /// The return value is a tuple that also includes the `authz`
    /// objects for all resources along the path to this one (i.e., all
    /// parent resources) and the authz object for this resource itself.
    /// These objects are useful for identifying those resources by
    /// id, for doing other authz checks, or for looking up related
    /// objects.
    pub async fn fetch_for(
        &self,
        action: authz::Action,
    ) -> LookupResult<(authz::Silo, authz::Project, nexus_db_model::Project)> {
        let lookup = self.lookup_root();
        let opctx = &lookup.opctx;
        let datastore = lookup.datastore;
        match self {
            Project::Error(_, error) => Err(error.clone()),
            &Project::Name(ref parent, &ref name)
            | &Project::OwnedName(ref parent, ref name) => {
                let (authz_silo,) = parent.lookup().await?;
                let (authz_project, db_row) = Self::fetch_by_name_for(
                        opctx,
                        datastore,
                        &authz_silo,
                        name,
                        action,
                    )
                    .await?;
                Ok((authz_silo, authz_project, db_row))
            }
            Project::PrimaryKey(_, v0) => {
                Self::fetch_by_id_for(opctx, datastore, v0, action).await
            }
        }
            .and_then(|input| {
                let &(ref authz_silo, .., ref authz_project, ref _db_row) = &input;
                Self::silo_check(opctx, authz_silo, authz_project)?;
                Ok(input)
            })
    }
    /// Turn the Result<T, E> of [`fetch_for`] into a Result<Option<T>, E>.
    pub async fn optional_fetch_for(
        &self,
        action: authz::Action,
    ) -> LookupResult<Option<(authz::Silo, authz::Project, nexus_db_model::Project)>> {
        let result = self.fetch_for(action).await;
        match result {
            Err(Error::ObjectNotFound { type_name: _, lookup_type: _ }) => Ok(None),
            _ => Ok(Some(result?)),
        }
    }
    /// Fetch an `authz` object for the selected resource and check
    /// whether the caller is allowed to do the specified `action`
    ///
    /// The return value is a tuple that also includes the `authz`
    /// objects for all resources along the path to this one (i.e., all
    /// parent resources) and the authz object for this resource itself.
    /// These objects are useful for identifying those resources by
    /// id, for doing other authz checks, or for looking up related
    /// objects.
    pub async fn lookup_for(
        &self,
        action: authz::Action,
    ) -> LookupResult<(authz::Silo, authz::Project)> {
        let lookup = self.lookup_root();
        let opctx = &lookup.opctx;
        let (authz_silo, authz_project) = self.lookup().await?;
        opctx.authorize(action, &authz_project).await?;
        Ok((authz_silo, authz_project))
            .and_then(|input| {
                let &(ref authz_silo, .., ref authz_project) = &input;
                Self::silo_check(opctx, authz_silo, authz_project)?;
                Ok(input)
            })
    }
    /// Turn the Result<T, E> of [`lookup_for`] into a Result<Option<T>, E>.
    pub async fn optional_lookup_for(
        &self,
        action: authz::Action,
    ) -> LookupResult<Option<(authz::Silo, authz::Project)>> {
        let result = self.lookup_for(action).await;
        match result {
            Err(Error::ObjectNotFound { type_name: _, lookup_type: _ }) => Ok(None),
            _ => Ok(Some(result?)),
        }
    }
    /// Fetch the "authz" objects for the selected resource and all its
    /// parents
    ///
    /// This function does not check whether the caller has permission
    /// to read this information.  That's why it's not `pub`.  Outside
    /// this module, you want `lookup_for(authz::Action)`.
    async fn lookup(&self) -> LookupResult<(authz::Silo, authz::Project)> {
        let lookup = self.lookup_root();
        let opctx = &lookup.opctx;
        let datastore = lookup.datastore;
        match self {
            Project::Error(_, error) => Err(error.clone()),
            &Project::Name(ref parent, &ref name)
            | &Project::OwnedName(ref parent, ref name) => {
                let (authz_silo,) = parent.lookup().await?;
                let (authz_project, _) = Self::lookup_by_name_no_authz(
                        opctx,
                        datastore,
                        &authz_silo,
                        name,
                    )
                    .await?;
                Ok((authz_silo, authz_project))
            }
            Project::PrimaryKey(_, v0) => {
                let (authz_silo, authz_project, _) = Self::lookup_by_id_no_authz(
                        opctx,
                        datastore,
                        v0,
                    )
                    .await?;
                Ok((authz_silo, authz_project))
            }
        }
    }
    /// Build the `authz` object for this resource
    fn make_authz(
        authz_parent: &authz::Silo,
        db_row: &nexus_db_model::Project,
        lookup_type: LookupType,
    ) -> authz::Project {
        authz::Project::with_primary_key(authz_parent.clone(), db_row.id(), lookup_type)
    }
    /// Getting the [`LookupPath`] for this lookup
    ///
    /// This is used when we actually query the database.  At that
    /// point, we need the `OpContext` and `DataStore` that are being
    /// used for this lookup.
    fn lookup_root(&self) -> &LookupPath<'a> {
        match &self {
            Project::Error(root, ..) => root.lookup_root(),
            Project::Name(parent, _) | Project::OwnedName(parent, _) => {
                parent.lookup_root()
            }
            Project::PrimaryKey(root, ..) => root.lookup_root(),
        }
    }
    /// For a "siloed" resource (i.e., one that's nested under "Silo" in
    /// the resource hierarchy), check whether a given resource's Silo
    /// (given by `authz_silo`) matches the Silo of the actor doing the
    /// fetch/lookup (given by `opctx`).
    ///
    /// This check should not be strictly necessary.  We should never
    /// wind up hitting the error conditions here.  That's because in
    /// order to reach this check, we must have done a successful authz
    /// check.  That check should have failed because there's no way to
    /// grant users access to resources in other Silos.  So why do this
    /// check at all?  As a belt-and-suspenders way to make sure we
    /// never return objects to a user that are from a different Silo
    /// than the one they're attached to.  But what do we do if the
    /// check fails?  We definitely want to know about it so that we can
    /// determine if there's an authz bug here, and if so, fix it.
    /// That's why we log this at "error" level.  We also override the
    /// lookup return value with a suitable error indicating the
    /// resource does not exist or the caller did not supply
    /// credentials, just as if they didn't have access to the object.
    fn silo_check(
        opctx: &OpContext,
        authz_silo: &authz::Silo,
        authz_project: &authz::Project,
    ) -> Result<(), Error> {
        let log = &opctx.log;
        let actor_silo_id = match opctx
            .authn
            .silo_or_builtin()
            .internal_context("siloed resource check")
        {
            Ok(Some(silo)) => silo.id(),
            Ok(None) => {
                trace!(
                    log,
                    "successful lookup of siloed resource {:?} \
                            using built-in user",
                    "Project",
                );
                return Ok(());
            }
            Err(error) => {
                error!(
                    log,
                    "unexpected successful lookup of siloed resource \
                            {:?} with no actor in OpContext",
                    "Project",
                );
                return Err(error);
            }
        };
        let resource_silo_id = authz_silo.id();
        if resource_silo_id != actor_silo_id {
            use nexus_auth::authz::ApiResource;
            error!(
                log,
                "unexpected successful lookup of siloed resource \
                        {:?} in a different Silo from current actor (resource \
                        Silo {}, actor Silo {})",
                "Project", resource_silo_id, actor_silo_id,
            );
            Err(authz_project.not_found())
        } else {
            Ok(())
        }
    }
    /// Fetch the database row for a resource by doing a lookup by
    /// name, possibly within a collection
    ///
    /// This function checks whether the caller has permissions to
    /// read the requested data.  However, it's not intended to be
    /// used outside this module.  See `fetch_for(authz::Action)`.
    async fn fetch_by_name_for(
        opctx: &OpContext,
        datastore: &dyn LookupDataStore,
        authz_silo: &authz::Silo,
        name: &Name,
        action: authz::Action,
    ) -> LookupResult<(authz::Project, nexus_db_model::Project)> {
        let (authz_project, db_row) = Self::lookup_by_name_no_authz(
                opctx,
                datastore,
                authz_silo,
                name,
            )
            .await?;
        opctx.authorize(action, &authz_project).await?;
        Ok((authz_project, db_row))
    }
    /// Lowest-level function for looking up a resource in the
    /// database by name, possibly within a collection
    ///
    /// This function does not check whether the caller has
    /// permission to read this information.  That's why it's not
    /// `pub`.  Outside this module, you want `fetch()` or
    /// `lookup_for(authz::Action)`.
    async fn lookup_by_name_no_authz(
        opctx: &OpContext,
        datastore: &dyn LookupDataStore,
        authz_silo: &authz::Silo,
        name: &Name,
    ) -> LookupResult<(authz::Project, nexus_db_model::Project)> {
        use ::nexus_db_schema::schema::project::dsl;
        dsl::project
            .filter(dsl::time_deleted.is_null())
            .filter(dsl::name.eq(name.clone()))
            .filter(dsl::silo_id.eq(authz_silo.id()))
            .select(nexus_db_model::Project::as_select())
            .get_result_async(&*datastore.pool_connection_authorized(opctx).await?)
            .await
            .map_err(|e| {
                public_error_from_diesel(
                    e,
                    ErrorHandler::NotFoundByLookup(
                        ResourceType::Project,
                        LookupType::ByName(name.as_str().to_string()),
                    ),
                )
            })
            .map(|db_row| {
                (
                    Self::make_authz(
                        authz_silo,
                        &db_row,
                        LookupType::ByName(name.as_str().to_string()),
                    ),
                    db_row,
                )
            })
    }
    /// Fetch the database row for a resource by doing a lookup by id
    ///
    /// This function checks whether the caller has permissions to read
    /// the requested data.  However, it's not intended to be used
    /// outside this module.  See `fetch_for(authz::Action)`.
    async fn fetch_by_id_for(
        opctx: &OpContext,
        datastore: &dyn LookupDataStore,
        v0: &Uuid,
        action: authz::Action,
    ) -> LookupResult<(authz::Silo, authz::Project, nexus_db_model::Project)> {
        let (authz_silo, authz_project, db_row) = Self::lookup_by_id_no_authz(
                opctx,
                datastore,
                v0,
            )
            .await?;
        opctx.authorize(action, &authz_project).await?;
        Ok((authz_silo, authz_project, db_row))
    }
    /// Lowest-level function for looking up a resource in the database
    /// by id
    ///
    /// This function does not check whether the caller has permission
    /// to read this information.  That's why it's not `pub`.  Outside
    /// this module, you want `fetch()` or `lookup_for(authz::Action)`.
    async fn lookup_by_id_no_authz(
        opctx: &OpContext,
        datastore: &dyn LookupDataStore,
        v0: &Uuid,
    ) -> LookupResult<(authz::Silo, authz::Project, nexus_db_model::Project)> {
        use ::nexus_db_schema::schema::project::dsl;
        let db_row = dsl::project
            .filter(dsl::time_deleted.is_null())
            .filter(dsl::id.eq(v0.clone()))
            .select(nexus_db_model::Project::as_select())
            .get_result_async(&*datastore.pool_connection_authorized(opctx).await?)
            .await
            .map_err(|e| {
                public_error_from_diesel(
                    e,
                    ErrorHandler::NotFoundByLookup(
                        ResourceType::Project,
                        LookupType::ById(
                            ::omicron_uuid_kinds::GenericUuid::into_untyped_uuid(*v0),
                        ),
                    ),
                )
            })?;
        let (authz_silo, _) = Silo::lookup_by_id_no_authz(
                opctx,
                datastore,
                &db_row.silo_id.into(),
            )
            .await?;
        let authz_project = Self::make_authz(
            &authz_silo,
            &db_row,
            LookupType::ById(::omicron_uuid_kinds::GenericUuid::into_untyped_uuid(*v0)),
        );
        Ok((authz_silo, authz_project, db_row))
    }
}
impl<'a> Silo<'a> {
    ///Select a resource of type Project within this Silo, identified by its name
    pub fn project_name<'b, 'c>(self, name: &'b Name) -> Project<'c>
    where
        'a: 'c,
        'b: 'c,
    {
        Project::Name(self, name)
    }
    ///Select a resource of type Project within this Silo, identified by its name
    pub fn project_name_owned<'c>(self, name: Name) -> Project<'c>
    where
        'a: 'c,
    {
        Project::OwnedName(self, name)
    }
}
