//! Authenticated caller identity endpoint.

use std::{collections::BTreeSet, sync::Arc};

use axum::{Extension, Json, http::StatusCode, response::IntoResponse};
use philharmonic_policy::{Principal, RoleDefinition, RoleMembership};
use philharmonic_store::EntityStore;
use philharmonic_types::Entity;
use serde::Serialize;

use crate::{
    ApiError, AuthContext, RequestContext,
    routes::identity::{
        bool_scalar, display_name, latest_revision, permissions_from_revision, required_entity_ref,
        store_error,
    },
    store::{ApiStore, ApiStoreHandle},
};

/// Shared state for the authenticated-caller identity endpoint.
#[derive(Clone)]
pub(crate) struct WhoamiState {
    store: ApiStoreHandle,
}

impl WhoamiState {
    pub(crate) fn new(store: Arc<dyn ApiStore>) -> Self {
        Self {
            store: ApiStoreHandle::new(store),
        }
    }
}

/// Response body for `/v1/whoami`.
#[derive(Debug, Serialize)]
pub struct WhoamiResponse {
    /// Public tenant UUID (use as `X-Tenant-Id` header value).
    pub tenant_id: uuid::Uuid,
    /// Authentication method (`"principal"` or `"ephemeral"`).
    pub auth_type: &'static str,
    /// Effective permission atoms after envelope/membership clipping.
    ///
    /// This is the set the route-protector enforces against.
    pub permissions: Vec<String>,
    /// Display name of the backing principal, when the caller is
    /// authenticated as a principal in the current tenant. `null`
    /// for ephemeral / cross-tenant / lookup-failed cases — clients
    /// should fall back to a non-name identifier.
    pub principal_display_name: Option<String>,
}

/// Return the authenticated caller's tenant identity.
pub async fn whoami(
    Extension(state): Extension<WhoamiState>,
    Extension(context): Extension<RequestContext>,
) -> Result<impl IntoResponse, ApiError> {
    let auth = context.auth.as_ref().ok_or(ApiError::Unauthenticated)?;
    let tenant_id = auth.tenant_id().public().as_uuid();
    let auth_type = if auth.is_principal() {
        "principal"
    } else {
        "ephemeral"
    };
    let principal_display_name = match auth {
        AuthContext::Principal {
            principal_id,
            tenant_id,
        } => principal_display_name_lookup(&state.store, *principal_id, *tenant_id).await?,
        AuthContext::Ephemeral { .. } => None,
    };
    Ok((
        StatusCode::OK,
        Json(WhoamiResponse {
            tenant_id,
            auth_type,
            permissions: effective_permissions(&state.store, auth).await?,
            principal_display_name,
        }),
    ))
}

async fn principal_display_name_lookup(
    store: &ApiStoreHandle,
    principal_id: philharmonic_types::EntityId<Principal>,
    tenant_id: philharmonic_types::EntityId<crate::Tenant>,
) -> Result<Option<String>, ApiError> {
    let revision = latest_revision::<Principal>(store, principal_id).await?;
    if required_entity_ref(&revision, "tenant")?.target_entity_id != tenant_id.internal().as_uuid()
    {
        return Ok(None);
    }
    Ok(Some(display_name(store, &revision, "principal").await?))
}

async fn effective_permissions(
    store: &ApiStoreHandle,
    auth: &AuthContext,
) -> Result<Vec<String>, ApiError> {
    let mut permissions = match auth {
        AuthContext::Principal {
            principal_id,
            tenant_id,
        } => principal_permissions(store, *principal_id, *tenant_id).await?,
        AuthContext::Ephemeral { permissions, .. } => permissions.clone(),
    };
    permissions.sort();
    permissions.dedup();
    Ok(permissions)
}

async fn principal_permissions(
    store: &ApiStoreHandle,
    principal_id: philharmonic_types::EntityId<Principal>,
    tenant_id: philharmonic_types::EntityId<crate::Tenant>,
) -> Result<Vec<String>, ApiError> {
    let principal_revision = latest_revision::<Principal>(store, principal_id).await?;
    if required_entity_ref(&principal_revision, "tenant")?.target_entity_id
        != tenant_id.internal().as_uuid()
        || bool_scalar(&principal_revision, "is_retired")?
    {
        return Ok(Vec::new());
    }

    let membership_refs = store
        .list_revisions_referencing(principal_id.internal().as_uuid(), "principal")
        .await
        .map_err(store_error)?;

    let mut permission_set = BTreeSet::new();
    for reference in membership_refs {
        let Some(entity) = store
            .get_entity(reference.entity_id)
            .await
            .map_err(store_error)?
        else {
            continue;
        };
        if entity.kind != RoleMembership::KIND {
            continue;
        }

        let Some(membership_revision) = store
            .get_latest_revision(reference.entity_id)
            .await
            .map_err(store_error)?
        else {
            continue;
        };
        if membership_revision.revision_seq != reference.revision_seq
            || bool_scalar(&membership_revision, "is_retired")?
            || required_entity_ref(&membership_revision, "tenant")?.target_entity_id
                != tenant_id.internal().as_uuid()
        {
            continue;
        }

        let role_ref = required_entity_ref(&membership_revision, "role")?;
        let Some(role_entity) = store
            .get_entity(role_ref.target_entity_id)
            .await
            .map_err(store_error)?
        else {
            continue;
        };
        if role_entity.kind != RoleDefinition::KIND {
            continue;
        }

        let role_id = role_entity
            .identity
            .typed::<RoleDefinition>()
            .map_err(|error| ApiError::Internal(format!("invalid role identity: {error}")))?;
        let role_revision = latest_revision::<RoleDefinition>(store, role_id).await?;
        if required_entity_ref(&role_revision, "tenant")?.target_entity_id
            != tenant_id.internal().as_uuid()
            || bool_scalar(&role_revision, "is_retired")?
        {
            continue;
        }

        for permission in permissions_from_revision(store, &role_revision, "permissions").await? {
            permission_set.insert(permission);
        }
    }

    Ok(permission_set.into_iter().collect())
}
