//! CRUD over the `api_keys` table — backs the `Authorization: Bearer
//! pic_…` credential flow from blueprint §11.6.
//!
//! The repo never sees the raw token; only the 8-char `prefix` and the
//! Argon2id `hash`. Mint logic (random-bytes generation, prefix split,
//! hash compute) lives in `api_keys_api.rs`. Verification logic
//! (prefix lookup + Argon2 verify per candidate) lives in
//! `auth_middleware.rs`. Both call this repo for the storage layer.

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use picloud_shared::{AdminUserId, ApiKeyId, AppId, Scope};
use sqlx::PgPool;

#[derive(Debug, thiserror::Error)]
pub enum ApiKeyRepositoryError {
    #[error("database error: {0}")]
    Db(#[from] sqlx::Error),

    #[error("api key not found: {0}")]
    NotFound(ApiKeyId),

    #[error("invalid scope stored in DB: {0}")]
    InvalidScope(String),
}

/// Insert payload — built by `api_keys_api` after generating the raw
/// token and hashing it. `hash` is an Argon2id PHC string covering the
/// body of the token (everything after `pic_`); `prefix` is the first
/// 8 chars of that body, indexed for fast candidate lookup.
#[derive(Debug, Clone)]
pub struct NewApiKey {
    pub user_id: AdminUserId,
    pub hash: String,
    pub prefix: String,
    pub name: String,
    pub scopes: Vec<Scope>,
    pub app_id: Option<AppId>,
    pub expires_at: Option<DateTime<Utc>>,
}

/// Public-facing row — never exposes the hash. Used for `GET
/// /admin/api-keys` and the `POST` response (alongside the
/// one-shot raw token).
#[derive(Debug, Clone)]
pub struct ApiKeyRow {
    pub id: ApiKeyId,
    pub user_id: AdminUserId,
    pub prefix: String,
    pub name: String,
    pub scopes: Vec<Scope>,
    pub app_id: Option<AppId>,
    pub expires_at: Option<DateTime<Utc>>,
    pub last_used_at: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
}

/// Verification candidate — includes the Argon2id `hash` and `user_id`
/// so middleware can verify the supplied token and assemble the
/// `Principal`. Kept separate from `ApiKeyRow` so handlers can't leak
/// the hash through a careless `Json(row)`.
#[derive(Debug, Clone)]
pub struct ApiKeyVerification {
    pub id: ApiKeyId,
    pub user_id: AdminUserId,
    pub hash: String,
    pub scopes: Vec<Scope>,
    pub app_id: Option<AppId>,
}

#[async_trait]
pub trait ApiKeyRepository: Send + Sync {
    /// Mint. Caller has already hashed the raw token + computed prefix.
    async fn create(&self, key: NewApiKey) -> Result<ApiKeyRow, ApiKeyRepositoryError>;

    /// Return every non-expired key with the given 8-char prefix. The
    /// caller (middleware) Argon2-verifies the supplied token against
    /// each candidate's `hash`. Returning a Vec rather than one row
    /// keeps the contract correct even if two keys happen to share a
    /// prefix (statistically near-zero but possible).
    async fn find_active_by_prefix(
        &self,
        prefix: &str,
    ) -> Result<Vec<ApiKeyVerification>, ApiKeyRepositoryError>;

    /// Update `last_used_at` for an authenticated request. Inline (not
    /// fire-and-forget) so a DB blip surfaces as a 500 rather than
    /// silent stale timestamps.
    async fn touch_last_used(&self, id: ApiKeyId) -> Result<(), ApiKeyRepositoryError>;

    /// Caller's own keys, for `GET /admin/api-keys`.
    async fn list_for_user(
        &self,
        user_id: AdminUserId,
    ) -> Result<Vec<ApiKeyRow>, ApiKeyRepositoryError>;

    /// Look up a key by id — used by `DELETE` to verify ownership
    /// before issuing the delete.
    async fn get(&self, id: ApiKeyId) -> Result<Option<ApiKeyRow>, ApiKeyRepositoryError>;

    /// Delete the row only if it belongs to `user_id`. Returns whether
    /// a row was actually deleted (false = key didn't exist OR wasn't
    /// theirs — handlers map both to 404 to avoid leaking the
    /// distinction).
    async fn delete_by_id_and_user(
        &self,
        id: ApiKeyId,
        user_id: AdminUserId,
    ) -> Result<bool, ApiKeyRepositoryError>;

    /// Set `expires_at = NOW()` on every active key for a user. Wired
    /// into `set_active(false)` so deactivation invalidates both
    /// sessions (already done by `AdminSessionRepository::delete_for_user`)
    /// and bearer keys at the same moment.
    async fn expire_all_for_user(&self, user_id: AdminUserId)
        -> Result<u64, ApiKeyRepositoryError>;
}

pub struct PostgresApiKeyRepository {
    pool: PgPool,
}

impl PostgresApiKeyRepository {
    #[must_use]
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

#[async_trait]
impl ApiKeyRepository for PostgresApiKeyRepository {
    async fn create(&self, key: NewApiKey) -> Result<ApiKeyRow, ApiKeyRepositoryError> {
        let scope_strings: Vec<String> =
            key.scopes.iter().map(|s| s.as_str().to_string()).collect();
        let row = sqlx::query_as::<_, ApiKeyRecord>(
            "INSERT INTO api_keys \
                (user_id, hash, prefix, name, scopes, app_id, expires_at) \
             VALUES ($1, $2, $3, $4, $5, $6, $7) \
             RETURNING id, user_id, prefix, name, scopes, app_id, \
                       expires_at, last_used_at, created_at",
        )
        .bind(key.user_id.into_inner())
        .bind(&key.hash)
        .bind(&key.prefix)
        .bind(&key.name)
        .bind(&scope_strings)
        .bind(key.app_id.map(picloud_shared::AppId::into_inner))
        .bind(key.expires_at)
        .fetch_one(&self.pool)
        .await?;
        row.try_into()
    }

    async fn find_active_by_prefix(
        &self,
        prefix: &str,
    ) -> Result<Vec<ApiKeyVerification>, ApiKeyRepositoryError> {
        let rows = sqlx::query_as::<_, ApiKeyVerifyRecord>(
            "SELECT id, user_id, hash, scopes, app_id \
             FROM api_keys \
             WHERE prefix = $1 \
               AND (expires_at IS NULL OR expires_at > NOW())",
        )
        .bind(prefix)
        .fetch_all(&self.pool)
        .await?;
        rows.into_iter().map(TryInto::try_into).collect()
    }

    async fn touch_last_used(&self, id: ApiKeyId) -> Result<(), ApiKeyRepositoryError> {
        sqlx::query("UPDATE api_keys SET last_used_at = NOW() WHERE id = $1")
            .bind(id.into_inner())
            .execute(&self.pool)
            .await?;
        Ok(())
    }

    async fn list_for_user(
        &self,
        user_id: AdminUserId,
    ) -> Result<Vec<ApiKeyRow>, ApiKeyRepositoryError> {
        let rows = sqlx::query_as::<_, ApiKeyRecord>(
            "SELECT id, user_id, prefix, name, scopes, app_id, \
                    expires_at, last_used_at, created_at \
             FROM api_keys WHERE user_id = $1 \
             ORDER BY created_at DESC",
        )
        .bind(user_id.into_inner())
        .fetch_all(&self.pool)
        .await?;
        rows.into_iter().map(TryInto::try_into).collect()
    }

    async fn get(&self, id: ApiKeyId) -> Result<Option<ApiKeyRow>, ApiKeyRepositoryError> {
        let row = sqlx::query_as::<_, ApiKeyRecord>(
            "SELECT id, user_id, prefix, name, scopes, app_id, \
                    expires_at, last_used_at, created_at \
             FROM api_keys WHERE id = $1",
        )
        .bind(id.into_inner())
        .fetch_optional(&self.pool)
        .await?;
        row.map(TryInto::try_into).transpose()
    }

    async fn delete_by_id_and_user(
        &self,
        id: ApiKeyId,
        user_id: AdminUserId,
    ) -> Result<bool, ApiKeyRepositoryError> {
        let res = sqlx::query("DELETE FROM api_keys WHERE id = $1 AND user_id = $2")
            .bind(id.into_inner())
            .bind(user_id.into_inner())
            .execute(&self.pool)
            .await?;
        Ok(res.rows_affected() > 0)
    }

    async fn expire_all_for_user(
        &self,
        user_id: AdminUserId,
    ) -> Result<u64, ApiKeyRepositoryError> {
        let res = sqlx::query(
            "UPDATE api_keys \
             SET expires_at = NOW() \
             WHERE user_id = $1 \
               AND (expires_at IS NULL OR expires_at > NOW())",
        )
        .bind(user_id.into_inner())
        .execute(&self.pool)
        .await?;
        Ok(res.rows_affected())
    }
}

#[derive(sqlx::FromRow)]
struct ApiKeyRecord {
    id: uuid::Uuid,
    user_id: uuid::Uuid,
    prefix: String,
    name: String,
    scopes: Vec<String>,
    app_id: Option<uuid::Uuid>,
    expires_at: Option<DateTime<Utc>>,
    last_used_at: Option<DateTime<Utc>>,
    created_at: DateTime<Utc>,
}

impl TryFrom<ApiKeyRecord> for ApiKeyRow {
    type Error = ApiKeyRepositoryError;
    fn try_from(r: ApiKeyRecord) -> Result<Self, Self::Error> {
        Ok(Self {
            id: r.id.into(),
            user_id: r.user_id.into(),
            prefix: r.prefix,
            name: r.name,
            scopes: parse_scopes(r.scopes)?,
            app_id: r.app_id.map(Into::into),
            expires_at: r.expires_at,
            last_used_at: r.last_used_at,
            created_at: r.created_at,
        })
    }
}

#[derive(sqlx::FromRow)]
struct ApiKeyVerifyRecord {
    id: uuid::Uuid,
    user_id: uuid::Uuid,
    hash: String,
    scopes: Vec<String>,
    app_id: Option<uuid::Uuid>,
}

impl TryFrom<ApiKeyVerifyRecord> for ApiKeyVerification {
    type Error = ApiKeyRepositoryError;
    fn try_from(r: ApiKeyVerifyRecord) -> Result<Self, Self::Error> {
        Ok(Self {
            id: r.id.into(),
            user_id: r.user_id.into(),
            hash: r.hash,
            scopes: parse_scopes(r.scopes)?,
            app_id: r.app_id.map(Into::into),
        })
    }
}

fn parse_scopes(raw: Vec<String>) -> Result<Vec<Scope>, ApiKeyRepositoryError> {
    raw.into_iter()
        .map(|s| Scope::from_wire(&s).ok_or(ApiKeyRepositoryError::InvalidScope(s)))
        .collect()
}
