//! `DeadLetterService` — Rhai SDK contract for replaying and resolving
//! dead letters. Surface kept intentionally narrow for v1.1.1 (no
//! `list` — deferred to v1.2 per `docs/v1.1.x-design-notes.md` §4).
//!
//! Both methods are gated by `Capability::AppDeadLetterManage(AppId)`
//! evaluated inside the impl. Public-HTTP scripts running with
//! `cx.principal = None` will fail the check, which matches the
//! design's expectation (managing dead letters is an admin act).

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;

use crate::SdkCallCx;

/// Opaque identifier for a `dead_letters` row.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DeadLetterId(pub Uuid);

impl DeadLetterId {
    #[must_use]
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }

    #[must_use]
    pub fn into_inner(self) -> Uuid {
        self.0
    }
}

impl Default for DeadLetterId {
    fn default() -> Self {
        Self::new()
    }
}

impl From<Uuid> for DeadLetterId {
    fn from(u: Uuid) -> Self {
        Self(u)
    }
}

impl From<DeadLetterId> for Uuid {
    fn from(id: DeadLetterId) -> Self {
        id.0
    }
}

impl std::fmt::Display for DeadLetterId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

#[async_trait]
pub trait DeadLetterService: Send + Sync {
    /// Re-enqueue the original event into the outbox. The dead-letter
    /// row is marked `resolution = 'replayed'` regardless of whether
    /// the retry ultimately succeeds.
    async fn replay(&self, cx: &SdkCallCx, id: DeadLetterId) -> Result<(), DeadLetterError>;

    /// Mark the row resolved with the given reason (typically
    /// `"ignored"` from the dashboard or `"handled_by_script"` from
    /// inside a `dead_letter` trigger handler).
    async fn resolve(
        &self,
        cx: &SdkCallCx,
        id: DeadLetterId,
        reason: &str,
    ) -> Result<(), DeadLetterError>;
}

#[derive(Debug, Error)]
pub enum DeadLetterError {
    #[error("dead-letter row not found")]
    NotFound,

    #[error("forbidden")]
    Forbidden,

    #[error("invalid resolution reason: {0}")]
    InvalidResolution(String),

    #[error("dead-letter backend error: {0}")]
    Backend(String),
}

/// Stub used to bootstrap the `Services` bundle before the real
/// Postgres-backed implementation lands. Behaves like
/// `NoopEventEmitter` — every call returns `Backend("...")` so scripts
/// see a clear "not yet implemented" error rather than silently
/// no-op'ing. Replaced by `PostgresDeadLetterService` in the v1.1.1
/// dead-letter PR.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopDeadLetterService;

#[async_trait]
impl DeadLetterService for NoopDeadLetterService {
    async fn replay(&self, _cx: &SdkCallCx, _id: DeadLetterId) -> Result<(), DeadLetterError> {
        Err(DeadLetterError::Backend(
            "dead_letters::replay is not yet wired in".into(),
        ))
    }

    async fn resolve(
        &self,
        _cx: &SdkCallCx,
        _id: DeadLetterId,
        _reason: &str,
    ) -> Result<(), DeadLetterError> {
        Err(DeadLetterError::Backend(
            "dead_letters::resolve is not yet wired in".into(),
        ))
    }
}
