//! `PrincipalResolver` — turns a `registered_by_principal` user id from
//! a trigger row into the `Principal` the dispatcher passes through to
//! the executor. Per design notes §4, a trigger execution runs as the
//! user that registered the trigger; the original event's caller is
//! recorded elsewhere (on the outbox row, for forensics) and does not
//! become the execution principal.

use async_trait::async_trait;
use picloud_shared::{AdminUserId, Principal};

use crate::admin_user_repo::{AdminUserRepository, AdminUserRepositoryError};

#[derive(Debug, thiserror::Error)]
pub enum PrincipalResolverError {
    #[error("user not found: {0}")]
    NotFound(AdminUserId),
    #[error("user is inactive: {0}")]
    Inactive(AdminUserId),
    #[error("admin user repo error: {0}")]
    Backend(String),
}

#[async_trait]
pub trait PrincipalResolver: Send + Sync {
    async fn resolve(&self, user_id: AdminUserId) -> Result<Principal, PrincipalResolverError>;
}

pub struct AdminPrincipalResolver {
    users: std::sync::Arc<dyn AdminUserRepository>,
}

impl AdminPrincipalResolver {
    #[must_use]
    pub fn new(users: std::sync::Arc<dyn AdminUserRepository>) -> Self {
        Self { users }
    }
}

#[async_trait]
impl PrincipalResolver for AdminPrincipalResolver {
    async fn resolve(&self, user_id: AdminUserId) -> Result<Principal, PrincipalResolverError> {
        let row = self
            .users
            .get(user_id)
            .await
            .map_err(|e: AdminUserRepositoryError| PrincipalResolverError::Backend(e.to_string()))?
            .ok_or(PrincipalResolverError::NotFound(user_id))?;
        if !row.is_active {
            return Err(PrincipalResolverError::Inactive(user_id));
        }
        Ok(Principal {
            user_id,
            instance_role: row.instance_role,
            // Trigger executions are cookie-session-style (no API key
            // scope restriction). Per-app permissions are evaluated
            // via `authz::can` against the `app_id` of the resource
            // the script touches, exactly like an admin invocation.
            scopes: None,
            app_binding: None,
        })
    }
}
