//! `/api/v1/admin/apps/{id}/dead_letters/*` — dashboard surface for
//! the no-default-handler model (design notes §4).
//!
//! Endpoints:
//! - `GET    /apps/{id}/dead_letters?unresolved=true` — list view
//! - `GET    /apps/{id}/dead_letters/count`           — badge count
//! - `GET    /apps/{id}/dead_letters/{dl_id}`         — row detail
//! - `POST   /apps/{id}/dead_letters/{dl_id}/replay`  — re-enqueue
//! - `POST   /apps/{id}/dead_letters/{dl_id}/resolve` — mark resolved
//!
//! All gated on `Capability::AppDeadLetterManage(app_id)`.

use std::sync::Arc;

use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json, Response};
use axum::routing::{get, post};
use axum::{Extension, Router};
use picloud_shared::{AppId, DeadLetterId, DeadLetterService, Principal, SdkCallCx};
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::app_repo::AppRepository;
use crate::authz::{require, AuthzDenied, AuthzError, AuthzRepo, Capability};
use crate::dead_letter_repo::{DeadLetterRepo, DeadLetterRepoError, DeadLetterRow};

#[derive(Clone)]
pub struct DeadLettersState {
    pub repo: Arc<dyn DeadLetterRepo>,
    pub service: Arc<dyn DeadLetterService>,
    pub apps: Arc<dyn AppRepository>,
    pub authz: Arc<dyn AuthzRepo>,
}

pub fn dead_letters_router(state: DeadLettersState) -> Router {
    Router::new()
        .route("/apps/{app_id}/dead_letters", get(list))
        .route("/apps/{app_id}/dead_letters/count", get(count))
        .route("/apps/{app_id}/dead_letters/{dl_id}", get(detail))
        .route("/apps/{app_id}/dead_letters/{dl_id}/replay", post(replay))
        .route("/apps/{app_id}/dead_letters/{dl_id}/resolve", post(resolve))
        .with_state(state)
}

#[derive(Debug, Deserialize)]
pub struct ListQuery {
    #[serde(default)]
    pub unresolved: bool,
    #[serde(default = "default_limit")]
    pub limit: i64,
    #[serde(default)]
    pub offset: i64,
}

const fn default_limit() -> i64 {
    50
}

#[derive(Debug, Serialize)]
pub struct ListResponse {
    pub dead_letters: Vec<DeadLetterDto>,
}

#[derive(Debug, Serialize)]
pub struct CountResponse {
    pub unresolved: i64,
}

#[derive(Debug, Deserialize)]
pub struct ResolveBody {
    pub reason: String,
}

#[derive(Debug, Serialize)]
pub struct DeadLetterDto {
    pub id: DeadLetterId,
    pub app_id: AppId,
    pub source: String,
    pub op: String,
    pub trigger_id: Option<picloud_shared::TriggerId>,
    pub script_id: Option<picloud_shared::ScriptId>,
    pub payload: serde_json::Value,
    pub attempt_count: u32,
    pub first_attempt_at: chrono::DateTime<chrono::Utc>,
    pub last_attempt_at: chrono::DateTime<chrono::Utc>,
    pub last_error: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub resolved_at: Option<chrono::DateTime<chrono::Utc>>,
    pub resolution: Option<String>,
}

impl From<DeadLetterRow> for DeadLetterDto {
    fn from(r: DeadLetterRow) -> Self {
        Self {
            id: r.id,
            app_id: r.app_id,
            source: r.source,
            op: r.op,
            trigger_id: r.trigger_id,
            script_id: r.script_id,
            payload: r.payload,
            attempt_count: r.attempt_count,
            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,
        }
    }
}

async fn list(
    State(s): State<DeadLettersState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
    Query(q): Query<ListQuery>,
) -> Result<Json<ListResponse>, DeadLettersApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppDeadLetterManage(app_id),
    )
    .await?;
    let rows = s
        .repo
        .list_for_app(app_id, q.unresolved, q.limit.clamp(1, 200), q.offset.max(0))
        .await?;
    Ok(Json(ListResponse {
        dead_letters: rows.into_iter().map(Into::into).collect(),
    }))
}

async fn count(
    State(s): State<DeadLettersState>,
    Extension(principal): Extension<Principal>,
    Path(id_or_slug): Path<String>,
) -> Result<Json<CountResponse>, DeadLettersApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppDeadLetterManage(app_id),
    )
    .await?;
    let n = s.repo.unresolved_count(app_id).await?;
    Ok(Json(CountResponse { unresolved: n }))
}

async fn detail(
    State(s): State<DeadLettersState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, dl_id)): Path<(String, DeadLetterId)>,
) -> Result<Json<DeadLetterDto>, DeadLettersApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    require(
        s.authz.as_ref(),
        &principal,
        Capability::AppDeadLetterManage(app_id),
    )
    .await?;
    let row = s
        .repo
        .get(dl_id)
        .await?
        .ok_or(DeadLettersApiError::NotFound(dl_id))?;
    if row.app_id != app_id {
        return Err(DeadLettersApiError::NotFound(dl_id));
    }
    Ok(Json(row.into()))
}

async fn replay(
    State(s): State<DeadLettersState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, dl_id)): Path<(String, DeadLetterId)>,
) -> Result<StatusCode, DeadLettersApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    // Authz handled inside the service via SdkCallCx.
    let cx = admin_cx(app_id, &principal);
    s.service
        .replay(&cx, dl_id)
        .await
        .map_err(map_service_err)?;
    Ok(StatusCode::NO_CONTENT)
}

async fn resolve(
    State(s): State<DeadLettersState>,
    Extension(principal): Extension<Principal>,
    Path((id_or_slug, dl_id)): Path<(String, DeadLetterId)>,
    Json(body): Json<ResolveBody>,
) -> Result<StatusCode, DeadLettersApiError> {
    let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
    let cx = admin_cx(app_id, &principal);
    s.service
        .resolve(&cx, dl_id, &body.reason)
        .await
        .map_err(map_service_err)?;
    Ok(StatusCode::NO_CONTENT)
}

/// Synthesize an `SdkCallCx` for the admin path. The service layer
/// reads `cx.app_id` + `cx.principal` and ignores the trigger /
/// execution fields, so the per-call ids are arbitrary.
fn admin_cx(app_id: AppId, principal: &Principal) -> SdkCallCx {
    SdkCallCx {
        app_id,
        // Admin-plane cx (dead-letter replay/resolve) — no script is
        // executing, so this attribution id is a fresh sentinel.
        script_id: picloud_shared::ScriptId::new(),
        principal: Some(principal.clone()),
        execution_id: picloud_shared::ExecutionId::new(),
        request_id: picloud_shared::RequestId::new(),
        trigger_depth: 0,
        root_execution_id: picloud_shared::ExecutionId::new(),
        is_dead_letter_handler: false,
        event: None,
    }
}

async fn resolve_app(apps: &dyn AppRepository, ident: &str) -> Result<AppId, DeadLettersApiError> {
    crate::app_repo::resolve_app(apps, ident)
        .await
        .map_err(|e| DeadLettersApiError::Backend(e.to_string()))?
        .map(|l| l.app.id)
        .ok_or_else(|| DeadLettersApiError::AppNotFound(ident.to_string()))
}

fn map_service_err(e: picloud_shared::DeadLetterError) -> DeadLettersApiError {
    match e {
        picloud_shared::DeadLetterError::NotFound => {
            DeadLettersApiError::NotFound(DeadLetterId::new())
        }
        picloud_shared::DeadLetterError::Forbidden => DeadLettersApiError::Forbidden,
        picloud_shared::DeadLetterError::InvalidResolution(s) => {
            DeadLettersApiError::Invalid(format!("invalid resolution: {s}"))
        }
        picloud_shared::DeadLetterError::Backend(s) => DeadLettersApiError::Backend(s),
    }
}

#[derive(Debug, thiserror::Error)]
pub enum DeadLettersApiError {
    #[error("app not found: {0}")]
    AppNotFound(String),

    #[error("dead-letter not found: {0}")]
    NotFound(DeadLetterId),

    #[error("invalid: {0}")]
    Invalid(String),

    #[error("forbidden")]
    Forbidden,

    #[error("authorization repo error: {0}")]
    AuthzRepo(String),

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

impl From<AuthzDenied> for DeadLettersApiError {
    fn from(d: AuthzDenied) -> Self {
        match d {
            AuthzDenied::Denied => Self::Forbidden,
            AuthzDenied::Repo(e) => Self::AuthzRepo(e.to_string()),
        }
    }
}

impl From<AuthzError> for DeadLettersApiError {
    fn from(e: AuthzError) -> Self {
        Self::AuthzRepo(e.to_string())
    }
}

impl From<DeadLetterRepoError> for DeadLettersApiError {
    fn from(e: DeadLetterRepoError) -> Self {
        match e {
            DeadLetterRepoError::NotFound(id) => Self::NotFound(id),
            DeadLetterRepoError::InvalidResolution(s) => Self::Invalid(s),
            DeadLetterRepoError::Db(e) => Self::Backend(e.to_string()),
        }
    }
}

impl IntoResponse for DeadLettersApiError {
    fn into_response(self) -> Response {
        let (status, body) = match &self {
            Self::AppNotFound(_) | Self::NotFound(_) => {
                (StatusCode::NOT_FOUND, json!({ "error": self.to_string() }))
            }
            Self::Invalid(_) => (
                StatusCode::UNPROCESSABLE_ENTITY,
                json!({ "error": self.to_string() }),
            ),
            Self::Forbidden => (StatusCode::FORBIDDEN, json!({ "error": self.to_string() })),
            Self::AuthzRepo(e) => {
                tracing::error!(error = %e, "dead_letters authz repo error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
            Self::Backend(e) => {
                tracing::error!(error = %e, "dead_letters api backend error");
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": "internal error" }),
                )
            }
        };
        (status, Json(body)).into_response()
    }
}
