//! Axum router for the provisioning-daemon REST API.
//!
//! Endpoints:
//!   GET  /health                   — liveness probe
//!   GET  /api/v1/tools             — list all registered tools
//!   POST /api/v1/tools/{name}      — invoke a tool (auth required)
//!   GET  /api/v1/state/recent      — recent invocations (auth required)
//!   GET  /api/v1/state/sessions    — active sessions (auth required)
//!   GET  /api/v1/state/stats       — aggregate statistics (auth required)

use axum::{
    Extension,
    Router,
    extract::{Path, State},
    http::{StatusCode, header},
    middleware,
    response::{IntoResponse, Json, Response},
    routing::{get, post},
};
use serde::Deserialize;
use serde_json::{Value, json};
use std::time::Instant;

use crate::{
    AppState,
    domain_state::{InvocationOutcome, ToolRecord},
    auth::Claims,
    http::middleware::{auth_layer, check_rbac},
};

/// Build the API router without calling `with_state` — callers can merge
/// this with other `Router<AppState>` routers before applying state.
pub fn raw_router(state: &AppState) -> Router<AppState> {
    let protected = Router::new()
        .route("/api/v1/tools/{name}", post(invoke_tool))
        .route("/api/v1/state/recent", get(recent_invocations))
        .route("/api/v1/state/sessions", get(active_sessions))
        .route("/api/v1/state/stats", get(stats))
        .route("/api/v1/ontology/templates", get(list_ontology_templates))
        .route("/api/v1/ontology/render/{template}", get(render_ontology))
        .layer(middleware::from_fn_with_state(state.clone(), auth_layer));

    Router::new()
        .route("/health", get(health))
        .route("/api/v1/tools", get(list_tools))
        .merge(protected)
        .merge(crate::http::admin::admin_router())
}

pub fn router(state: AppState) -> Router {
    raw_router(&state).with_state(state)
}

// ── Handlers ──────────────────────────────────────────────────────────────────

async fn health(State(app): State<AppState>) -> impl IntoResponse {
    Json(json!({
        "status": "ok",
        "tools": app.registry.len(),
        "invocations": app.state.total_invocations(),
    }))
}

async fn list_tools(State(app): State<AppState>) -> impl IntoResponse {
    let tools: Vec<Value> = app
        .registry
        .list()
        .into_iter()
        .map(|m| json!({ "name": m.name, "description": m.description, "category": format!("{:?}", m.category) }))
        .collect();
    Json(json!({ "tools": tools, "total": tools.len() }))
}

#[derive(Deserialize)]
struct InvokePayload {
    #[serde(default)]
    params: Value,
    /// Caller's session ID (optional).
    session_id: Option<String>,
}

async fn invoke_tool(
    State(app): State<AppState>,
    Extension(claims): Extension<Claims>,
    Path(name): Path<String>,
    Json(body): Json<InvokePayload>,
) -> impl IntoResponse {
    // Resolve metadata to check RBAC before execution.
    let meta = match app.registry.get(&name) {
        Some(m) => m,
        None => {
            return (
                StatusCode::NOT_FOUND,
                Json(json!({"error": format!("tool '{}' not found", name)})),
            ).into_response()
        }
    };

    if let Err(resp) = check_rbac(&claims, meta.category()) {
        return *resp;
    }

    let ctx = provisioning_core::tool::Context::new(std::sync::Arc::clone(&app.env));
    let t0 = Instant::now();
    let outcome = app.registry.invoke(&name, body.params.clone(), &ctx).await;
    let duration_ms = t0.elapsed().as_millis() as u64;

    let (record_outcome, resp) = match outcome {
        Ok(value) => (
            InvocationOutcome::Ok { value: value.clone() },
            (StatusCode::OK, Json(json!({ "result": value }))).into_response(),
        ),
        Err(e) => {
            let (code, msg) = tool_error_code(&e);
            (
                InvocationOutcome::Err { code, message: msg.clone() },
                (StatusCode::UNPROCESSABLE_ENTITY, Json(json!({ "error": msg, "code": code }))).into_response(),
            )
        }
    };

    let record = ToolRecord {
        tool: name.clone(),
        params: body.params,
        outcome: record_outcome,
        duration_ms,
        invoked_at: chrono::Utc::now(),
    };

    app.state.record_invocation(record.clone());

    if let Some(ref sid) = body.session_id {
        // Silently ignore when the session doesn't belong to the caller —
        // don't leak whether the session exists.
        let _ = app.state.increment_session_tool_count(sid, &claims.sub);
    }

    if let Some(ref bus) = app.bus {
        let ev = match &record.outcome {
            InvocationOutcome::Ok { value } => {
                crate::events::EventBus::tool_ok(&name, record.params.clone(), value.clone(), duration_ms)
            }
            InvocationOutcome::Err { code, message } => {
                crate::events::EventBus::tool_err(&name, record.params.clone(), *code, message.clone(), duration_ms)
            }
        };
        if let Err(e) = bus.publish(&ev).await {
            tracing::warn!("failed to publish tool event: {e}");
        }
    }

    resp
}

async fn recent_invocations(
    State(app): State<AppState>,
    Extension(_claims): Extension<Claims>,
) -> impl IntoResponse {
    let records = app.state.recent(50);
    Json(json!({ "invocations": records, "count": records.len() }))
}

async fn active_sessions(
    State(app): State<AppState>,
    Extension(_claims): Extension<Claims>,
) -> impl IntoResponse {
    let sessions = app.state.sessions();
    Json(json!({ "sessions": sessions, "count": sessions.len() }))
}

async fn stats(
    State(app): State<AppState>,
    Extension(_claims): Extension<Claims>,
) -> impl IntoResponse {
    Json(json!({
        "total_invocations": app.state.total_invocations(),
        "active_sessions":   app.state.sessions().len(),
        "secs_since_config_reload": app.state.secs_since_reload(),
    }))
}

// ── Ontology handlers ─────────────────────────────────────────────────────────

async fn list_ontology_templates(
    State(app): State<AppState>,
    Extension(_claims): Extension<Claims>,
) -> impl IntoResponse {
    match app.ontology.as_ref() {
        Some(r) => Json(json!({ "templates": r.template_names() })).into_response(),
        None => (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(json!({"error": "ontology templates not loaded"})),
        )
            .into_response(),
    }
}

async fn render_ontology(
    State(app): State<AppState>,
    Extension(_claims): Extension<Claims>,
    Path(template): Path<String>,
) -> Response {
    let Some(renderer) = app.ontology.as_ref() else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(json!({"error": "ontology templates not loaded"})),
        )
            .into_response();
    };
    let ctx = crate::ontology::build_context(&app.project_name, &app.registry, &app.state);
    match renderer.render(&template, &ctx) {
        Ok(text) => (
            StatusCode::OK,
            [(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
            text,
        )
            .into_response(),
        Err(e) => (
            StatusCode::UNPROCESSABLE_ENTITY,
            Json(json!({"error": e.to_string()})),
        )
            .into_response(),
    }
}

// ── Error mapping ─────────────────────────────────────────────────────────────

fn tool_error_code(e: &provisioning_core::ToolError) -> (i32, String) {
    match e {
        provisioning_core::ToolError::NotFound(_)          => (-32001, e.to_string()),
        provisioning_core::ToolError::InvalidParam { .. }  => (-32602, e.to_string()),
        provisioning_core::ToolError::Unauthorized(_)      => (-32003, e.to_string()),
        _                                                   => (-32000, e.to_string()),
    }
}
