//! `GroupDeadLetterRepo` — READ side over `group_dead_letters` (§11.6 D3).
//!
//! The group counterpart to the per-app [`crate::dead_letter_repo::DeadLetterRepo`]
//! read surface, keyed by `(group_id, collection)`. The WRITE side lives in
//! [`crate::group_queue_repo::GroupQueueRepo::dead_letter`] (co-located with the
//! queue-message delete for one-transaction atomicity, exactly as the per-app
//! `queue_repo::dead_letter` inlines its own INSERT). This repo backs the
//! read-only operator surface GET /api/v1/admin/groups/{id}/dead-letters, so an
//! operator can see shared-queue messages that exhausted their retries rather
//! than having them vanish.

use async_trait::async_trait;
use chrono::{DateTime, Utc};
use picloud_shared::{DeadLetterId, GroupId, ScriptId, TriggerId};
use sqlx::PgPool;
use uuid::Uuid;

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

/// One group dead-letter row (mirror of `dead_letter_repo::DeadLetterRow`,
/// group-keyed).
#[derive(Debug, Clone)]
pub struct GroupDeadLetterRow {
    pub id: DeadLetterId,
    pub group_id: GroupId,
    pub collection: String,
    pub original_event_id: Uuid,
    pub source: String,
    pub op: String,
    pub trigger_id: Option<TriggerId>,
    pub script_id: Option<ScriptId>,
    pub payload: serde_json::Value,
    pub attempt_count: u32,
    pub first_attempt_at: DateTime<Utc>,
    pub last_attempt_at: DateTime<Utc>,
    pub last_error: String,
    pub created_at: DateTime<Utc>,
    pub resolved_at: Option<DateTime<Utc>>,
    pub resolution: Option<String>,
}

#[async_trait]
pub trait GroupDeadLetterRepo: Send + Sync {
    /// A group's dead-letters, newest-first, capped at `limit` (bounded by the
    /// caller). `unresolved_only` filters to `resolved_at IS NULL`.
    async fn list_for_group(
        &self,
        group_id: GroupId,
        unresolved_only: bool,
        limit: i64,
    ) -> Result<Vec<GroupDeadLetterRow>, GroupDeadLetterRepoError>;
}

pub struct PostgresGroupDeadLetterRepo {
    pool: PgPool,
}

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

const COLS: &str = "id, group_id, collection, original_event_id, source, op, \
     trigger_id, script_id, payload, attempt_count, first_attempt_at, \
     last_attempt_at, last_error, created_at, resolved_at, resolution";

#[async_trait]
impl GroupDeadLetterRepo for PostgresGroupDeadLetterRepo {
    async fn list_for_group(
        &self,
        group_id: GroupId,
        unresolved_only: bool,
        limit: i64,
    ) -> Result<Vec<GroupDeadLetterRow>, GroupDeadLetterRepoError> {
        let rows: Vec<RawRow> = sqlx::query_as(&format!(
            "SELECT {COLS} FROM group_dead_letters \
             WHERE group_id = $1 AND ($2 = FALSE OR resolved_at IS NULL) \
             ORDER BY created_at DESC LIMIT $3"
        ))
        .bind(group_id.into_inner())
        .bind(unresolved_only)
        .bind(limit)
        .fetch_all(&self.pool)
        .await?;
        Ok(rows.into_iter().map(Into::into).collect())
    }
}

#[derive(sqlx::FromRow)]
struct RawRow {
    id: Uuid,
    group_id: Uuid,
    collection: String,
    original_event_id: Uuid,
    source: String,
    op: String,
    trigger_id: Option<Uuid>,
    script_id: Option<Uuid>,
    payload: serde_json::Value,
    attempt_count: i32,
    first_attempt_at: DateTime<Utc>,
    last_attempt_at: DateTime<Utc>,
    last_error: String,
    created_at: DateTime<Utc>,
    resolved_at: Option<DateTime<Utc>>,
    resolution: Option<String>,
}

impl From<RawRow> for GroupDeadLetterRow {
    fn from(r: RawRow) -> Self {
        Self {
            id: DeadLetterId(r.id),
            group_id: r.group_id.into(),
            collection: r.collection,
            original_event_id: r.original_event_id,
            source: r.source,
            op: r.op,
            trigger_id: r.trigger_id.map(Into::into),
            script_id: r.script_id.map(Into::into),
            payload: r.payload,
            attempt_count: u32::try_from(r.attempt_count).unwrap_or(0),
            first_attempt_at: r.first_attempt_at,
            last_attempt_at: r.last_attempt_at,
            last_error: r.last_error,
            created_at: r.created_at,
            resolved_at: r.resolved_at,
            resolution: r.resolution,
        }
    }
}
