//! Generic, manifest-driven HTTP + WebSocket router.
//!
//! Rather than registering a bespoke route per entity, a single set of routes
//! carries the table as a path parameter and looks the entity up in the
//! manifest at request time. This keeps the surface Payload-like: the same
//! handlers serve every collection, gated by each field's declared policy.

use crate::state::AppState;
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::{Json, Router};
use roundabout_orm_schema::EntityDef;
use serde_json::{json, Value};
use tokio::sync::broadcast::Receiver;

pub fn build(state: AppState) -> Router {
    let mut router = Router::new()
        .route("/{table}", get(list))
        .route("/{table}/{id}", get(get_one).patch(update))
        .route("/ws/{table}", get(ws));

    // Interactive docs at /docs + spec at /openapi.json (on unless disabled).
    // Static paths outrank the `/{table}` param route in axum's matchit.
    if std::env::var("ROUND_ABOUT_DOCS").map(|v| v != "off").unwrap_or(true) {
        router = router
            .route("/openapi.json", get(crate::docs::openapi_json))
            .route("/docs", get(crate::docs::index))
            .route("/docs/swagger-ui.css", get(crate::docs::css))
            .route("/docs/swagger-ui-bundle.js", get(crate::docs::js));
        println!("[roundabout_orm] API docs at http://127.0.0.1:8080/docs");
    }

    router.with_state(state)
}

/// Resolve a table name to its entity definition, or a 404.
fn entity<'a>(state: &'a AppState, table: &str) -> Result<&'a EntityDef, (StatusCode, String)> {
    state
        .manifest
        .iter()
        .find(|e| e.table == table)
        .ok_or_else(|| (StatusCode::NOT_FOUND, format!("unknown collection '{table}'")))
}

fn err(code: StatusCode, msg: impl Into<String>) -> (StatusCode, String) {
    (code, msg.into())
}

// GET /{table} -> array of readable fields (Postgres).
async fn list(
    State(state): State<AppState>,
    Path(table): Path<String>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let ent = entity(&state, &table)?;
    crate::db::postgres::read_many(&state.pg, ent, 100)
        .await
        .map(Json)
        .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}

// GET /{table}/{id} -> single row of readable fields (Postgres).
async fn get_one(
    State(state): State<AppState>,
    Path((table, id)): Path<(String, String)>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let ent = entity(&state, &table)?;
    match crate::db::postgres::read_one(&state.pg, ent, &id).await {
        Ok(Some(v)) => Ok(Json(v)),
        Ok(None) => Err(err(StatusCode::NOT_FOUND, "row not found")),
        Err(e) => Err(err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
    }
}

// PATCH /{table}/{id} -> validate against field policy, stage in SQLite (202).
async fn update(
    State(state): State<AppState>,
    Path((table, id)): Path<(String, String)>,
    Json(body): Json<Value>,
) -> Result<Response, (StatusCode, String)> {
    let ent = entity(&state, &table)?;
    let obj = body
        .as_object()
        .ok_or_else(|| err(StatusCode::BAD_REQUEST, "body must be a JSON object"))?;
    if obj.is_empty() {
        return Err(err(StatusCode::BAD_REQUEST, "no fields to update"));
    }

    // Reject anything the field policy does not allow updating.
    for key in obj.keys() {
        match ent.field(key) {
            None => return Err(err(StatusCode::BAD_REQUEST, format!("unknown field '{key}'"))),
            Some(f) if !f.update => {
                return Err(err(
                    StatusCode::BAD_REQUEST,
                    format!("field '{key}' is not updatable"),
                ));
            }
            Some(_) => {}
        }
    }

    let payload = serde_json::to_string(&body).unwrap();
    let outbox_id = crate::db::sqlite::enqueue(
        &state.sqlite,
        ent.name.clone(),
        ent.table.clone(),
        id.clone(),
        payload,
    )
    .await
    .map_err(|e| err(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    Ok((
        StatusCode::ACCEPTED,
        Json(json!({ "status": "staged", "outbox_id": outbox_id, "row_id": id })),
    )
        .into_response())
}

// GET /ws/{table} -> subscribe to live alerts (only for entities with ws fields).
async fn ws(
    State(state): State<AppState>,
    Path(table): Path<String>,
    upgrade: WebSocketUpgrade,
) -> Response {
    match state.ws_hub.get(&table) {
        Some(tx) => {
            let rx = tx.subscribe();
            upgrade.on_upgrade(move |socket| pump(socket, rx))
        }
        None => (
            StatusCode::NOT_FOUND,
            format!("collection '{table}' has no live fields"),
        )
            .into_response(),
    }
}

/// Forward broadcast alerts to the socket until either side closes.
async fn pump(mut socket: WebSocket, mut rx: Receiver<String>) {
    loop {
        tokio::select! {
            msg = rx.recv() => match msg {
                Ok(text) => {
                    if socket.send(Message::Text(text.into())).await.is_err() {
                        break;
                    }
                }
                Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
                Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
            },
            client = socket.recv() => match client {
                Some(Ok(Message::Close(_))) | None => break,
                Some(Err(_)) => break,
                _ => {}
            },
        }
    }
}
