use crate::api::ab::rules::{enforce, Rule};
use crate::api::error::ApiError;
use crate::api::middleware::AuthedUser;
use crate::api::pagination::Page;
use crate::api::state::AppState;
use crate::database::AbPeerInsert;
use axum::extract::{Extension, Path, Query};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;

/// `serde_urlencoded` (axum's query decoder) does not honour
/// `#[serde(flatten)]`, so the pagination fields are spelled out inline.
#[derive(Debug, Deserialize)]
pub struct AbQuery {
    /// guid sent in the query string for `/api/ab/peers?ab=<guid>`.
    pub ab: String,
    #[serde(default = "default_current")]
    pub current: i64,
    #[serde(default = "default_page_size", rename = "pageSize")]
    pub page_size: i64,
}

fn default_current() -> i64 {
    1
}
fn default_page_size() -> i64 {
    100
}

impl AbQuery {
    fn offset(&self) -> i64 {
        (self.current.max(1) - 1) * self.limit()
    }
    fn limit(&self) -> i64 {
        self.page_size.clamp(1, 1000)
    }
}

/// `POST /api/ab/peers?ab=<guid>` — paginated peer list inside an AB.
/// Wire shape matches the Flutter `Peer` decoder; only fields documented in
/// CONSOLE_API.md §4.4 are surfaced.
#[derive(Debug, Serialize)]
struct PeerOut {
    id: String,
    alias: String,
    tags: Vec<String>,
    note: String,
    #[serde(skip_serializing_if = "String::is_empty")]
    password: String,
    #[serde(skip_serializing_if = "String::is_empty")]
    hash: String,
    #[serde(skip_serializing_if = "String::is_empty")]
    username: String,
    #[serde(skip_serializing_if = "String::is_empty")]
    hostname: String,
    #[serde(skip_serializing_if = "String::is_empty")]
    platform: String,
}

pub async fn list(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Query(q): Query<AbQuery>,
) -> Result<impl IntoResponse, ApiError> {
    enforce(&state, user.user_id, &q.ab, Rule::Read).await?;
    let (total, rows) = state
        .db
        .ab_list_peers(&q.ab, q.offset(), q.limit())
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let data: Vec<PeerOut> = rows
        .into_iter()
        .map(|r| PeerOut {
            id: r.id,
            alias: r.alias,
            tags: r.tags,
            note: r.note,
            password: r.password,
            hash: r.hash,
            username: r.username,
            hostname: r.hostname,
            platform: r.platform,
        })
        .collect();
    Ok((StatusCode::OK, Json(Page { total, data })))
}

#[derive(Debug, Deserialize)]
pub struct PeerAddBody {
    pub id: String,
    #[serde(default)]
    pub alias: Option<String>,
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    #[serde(default)]
    pub note: Option<String>,
    #[serde(default)]
    pub password: Option<String>,
    #[serde(default)]
    pub hash: Option<String>,
    #[serde(default)]
    pub username: Option<String>,
    #[serde(default)]
    pub hostname: Option<String>,
    #[serde(default)]
    pub platform: Option<String>,
}

/// `POST /api/ab/peer/add/{guid}` — insert one peer. **Returns HTTP 200
/// with an empty body on success**, or `{"error":"..."}` JSON body on failure
/// (also HTTP 200). The Flutter `_jsonDecodeActionResp` at
/// flutter/lib/models/ab_model.dart:2002 treats *any* non-empty success body
/// as an error to surface — including `{}` (which produces the literal string
/// "null"), so action endpoints must reply with truly empty bodies.
pub async fn add(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Path(guid): Path<String>,
    Json(body): Json<PeerAddBody>,
) -> Result<StatusCode, ApiError> {
    enforce(&state, user.user_id, &guid, Rule::ReadWrite).await?;
    if body.id.is_empty() {
        return Err(ApiError::BadRequest("id required".into()));
    }
    let max = state.cfg.ab_max_peers_per_book;
    let count = state
        .db
        .ab_count_peers(&guid)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if count >= max {
        return Err(ApiError::Forbidden("exceed_max_devices".into()));
    }
    state
        .db
        .ab_peer_insert(
            &guid,
            AbPeerInsert {
                id: &body.id,
                alias: body.alias.as_deref(),
                note: body.note.as_deref(),
                password: body.password.as_deref(),
                hash: body.hash.as_deref(),
                username: body.username.as_deref(),
                hostname: body.hostname.as_deref(),
                platform: body.platform.as_deref(),
            },
            body.tags.as_deref(),
        )
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(StatusCode::OK)
}

/// `PUT /api/ab/peer/update/{guid}` — partial update. Body always carries
/// `id`, plus any subset of mutable fields. Empty success body, see `add`.
pub async fn update(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Path(guid): Path<String>,
    Json(body): Json<Value>,
) -> Result<StatusCode, ApiError> {
    enforce(&state, user.user_id, &guid, Rule::ReadWrite).await?;
    let id = body
        .get("id")
        .and_then(|v| v.as_str())
        .ok_or_else(|| ApiError::BadRequest("id required".into()))?;
    let updated = state
        .db
        .ab_peer_partial_update(&guid, id, &body)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if !updated {
        return Err(ApiError::Forbidden("peer not found".into()));
    }
    Ok(StatusCode::OK)
}

/// `DELETE /api/ab/peer/{guid}` — body is a JSON array of peer IDs. Empty
/// success body, see `add`.
pub async fn delete(
    Extension(state): Extension<Arc<AppState>>,
    user: AuthedUser,
    Path(guid): Path<String>,
    Json(ids): Json<Vec<String>>,
) -> Result<StatusCode, ApiError> {
    enforce(&state, user.user_id, &guid, Rule::ReadWrite).await?;
    state
        .db
        .ab_peers_delete(&guid, &ids)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(StatusCode::OK)
}
