use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum::Json;
use serde::Deserialize;
use serde_json::json;

use crate::api::AppState;

/// Push payload from a project client.
///
/// `slug` identifies the project. Must match a registered entry in the
/// registry. All ontology fields are optional — a client may push a partial
/// update.
#[derive(Debug, Deserialize)]
pub struct SyncPayload {
    /// Project slug — must match a registered ProjectContext.
    pub slug: String,
    pub core: Option<serde_json::Value>,
    pub state: Option<serde_json::Value>,
    pub gate: Option<serde_json::Value>,
}

/// Push-based sync — remote projects POST their pre-exported NCL payload to
/// upsert into the daemon cache.
///
/// `POST /sync`
///
/// Authentication:
/// - Looks up the project by `payload.slug` in the registry.
/// - If the project has `keys` configured, requires `Authorization: Bearer
///   <password>`. The password is verified with argon2id against the stored PHC
///   hash.
/// - If the project has no keys AND `push_only = true`, the push is rejected
///   (401): remote projects must always configure at least one key.
/// - Local projects (`push_only = false`) with no keys are accepted without
///   auth (assumed to be loopback/trusted network only).
#[ontoref_derive::onto_api(
    method = "POST",
    path = "/sync",
    auth = "viewer",
    actors = "ci, agent",
    params = "slug:string:required:Project slug from Authorization header context",
    tags = "sync, federation"
)]
pub async fn sync_push(
    State(state): State<AppState>,
    headers: HeaderMap,
    Json(payload): Json<SyncPayload>,
) -> impl IntoResponse {
    // ── Resolve project context ──────────────────────────────────────────────
    let ctx = match state.registry.get(&payload.slug) {
        Some(c) => c,
        None => {
            return (
                StatusCode::NOT_FOUND,
                Json(json!({"error": format!("project '{}' not registered", payload.slug)})),
            )
                .into_response();
        }
    };

    // ── Authentication ───────────────────────────────────────────────────────
    {
        if ctx.push_only && !ctx.auth_enabled() {
            // Remote projects with no keys configured are rejected outright.
            // This prevents accidental open push endpoints for remote registrations.
            return (
                StatusCode::UNAUTHORIZED,
                Json(json!({
                    "error": "remote project has no keys configured — push rejected",
                    "hint": "add at least one key to this project entry in remote-projects.ncl"
                })),
            )
                .into_response();
        }

        if ctx.auth_enabled() {
            let bearer = crate::api::extract_bearer(&headers);
            match bearer {
                None => {
                    return (
                        StatusCode::UNAUTHORIZED,
                        Json(json!({"error": "Authorization: Bearer <password> required"})),
                    )
                        .into_response();
                }
                Some(password) => {
                    if ctx.verify_key(password).is_none() {
                        return (
                            StatusCode::UNAUTHORIZED,
                            Json(json!({"error": "invalid credentials"})),
                        )
                            .into_response();
                    }
                }
            }
        }
    }

    handle_sync_push(&state, &payload, headers).await
}

/// Inner handler — called after auth is satisfied.
async fn handle_sync_push(
    state: &AppState,
    payload: &SyncPayload,
    _headers: HeaderMap,
) -> axum::response::Response {
    #[cfg(feature = "db")]
    {
        let Some(ref db) = state.db else {
            return (
                StatusCode::SERVICE_UNAVAILABLE,
                Json(json!({"error": "db not enabled — start daemon with db feature"})),
            )
                .into_response();
        };

        let value = serde_json::json!({
            "core":  payload.core,
            "state": payload.state,
            "gate":  payload.gate,
        });

        // Acquire the project's seed_lock before seeding to prevent concurrent
        // re-seeds from overlapping with filesystem-triggered seeds on the same
        // project.
        let ctx = match state.registry.get(&payload.slug) {
            Some(c) => c,
            None => {
                return (
                    StatusCode::NOT_FOUND,
                    Json(json!({"error": format!("project '{}' not registered", payload.slug)})),
                )
                    .into_response();
            }
        };
        let (report, version_bump) = {
            let permit = match ctx.seed_lock.acquire().await {
                Ok(p) => p,
                Err(_) => {
                    return (
                        StatusCode::SERVICE_UNAVAILABLE,
                        Json(json!({"error": "seed_lock closed — daemon shutting down"})),
                    )
                        .into_response();
                }
            };
            let r = crate::seed::seed_from_payload(db, &payload.slug, &value).await;
            drop(permit);
            let v = ctx
                .ontology_version
                .fetch_add(1, std::sync::atomic::Ordering::Release)
                + 1;
            (r, v)
        };

        (
            StatusCode::OK,
            Json(serde_json::json!({
                "slug":       payload.slug,
                "nodes":      report.nodes,
                "edges":      report.edges,
                "dimensions": report.dimensions,
                "membranes":  report.membranes,
                "ontology_version": version_bump,
            })),
        )
            .into_response()
    }

    #[cfg(not(feature = "db"))]
    {
        let _ = state;
        let _ = payload;
        (
            StatusCode::NOT_IMPLEMENTED,
            Json(json!({"error": "compiled without db feature"})),
        )
            .into_response()
    }
}
