//! H4: REST API server — `p3ak-vault serve --port 8080`
//!
//! Exposes the vault over HTTP (127.0.0.1 only, no TLS in v1).
//!
//! # Routes
//!
//! | Method | Path | Description |
//! |--------|------|-------------|
//! | GET | `/health` | Liveness check |
//! | GET | `/search?q=...&mode=hybrid&limit=5&store=FN&room=work` | Search |
//! | GET | `/docs` | All ingested documents |
//! | GET | `/wal` | Audit log |
//! | POST | `/ingest` `{"file":"/path","room":"work"}` | Ingest a file |
//! | POST | `/write` `{"type":"goal","payload":{...}}` | Write a structured entry |
//! | POST | `/classify` `{"only_missing":true}` | Classify documents |

use anyhow::Result;
use std::path::Path;
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};

/// Start a blocking HTTP server that exposes the vault.
///
/// Binds to `127.0.0.1:<port>`. Press Ctrl-C to stop.
pub fn cmd_serve(vault_path: &Path, port: u16, passphrase: Option<String>) -> Result<()> {
    let addr = format!("127.0.0.1:{port}");
    let server = Server::http(&addr).map_err(|e| anyhow::anyhow!("failed to bind {addr}: {e}"))?;

    eprintln!(
        "p3ak-vault serve: listening on http://{addr}\n\
         Vault: {}\n\
         Press Ctrl-C to stop.",
        vault_path.display()
    );

    println!(
        "{}",
        serde_json::json!({
            "event": "started",
            "address": format!("http://{addr}"),
            "vault": vault_path.display().to_string(),
            "encrypted": passphrase.is_some(),
        })
    );

    for mut request in server.incoming_requests() {
        // Pre-read the body for POST requests before routing.
        let body = if *request.method() == Method::Post {
            let mut buf = String::new();
            let _ = request.as_reader().read_to_string(&mut buf);
            buf
        } else {
            String::new()
        };

        let (status, body_json) = handle(vault_path, passphrase.as_deref(), &request, &body);
        let _ = send_response(request, status, body_json);
    }
    Ok(())
}

// ─── Router ───────────────────────────────────────────────────────────────────

fn handle(
    vault_path: &Path,
    passphrase: Option<&str>,
    req: &Request,
    body: &str,
) -> (StatusCode, serde_json::Value) {
    let url = req.url().to_string();
    let method = req.method().clone();

    let (path_str, query_str) = url.split_once('?').unwrap_or((url.as_str(), ""));

    match (method, path_str) {
        (Method::Get, "/health") => handle_health(vault_path, passphrase),
        (Method::Get, "/search") => handle_search(vault_path, passphrase, query_str),
        (Method::Get, "/docs") => handle_read_docs(vault_path, passphrase),
        (Method::Get, "/wal") => handle_read_wal(vault_path, passphrase),
        (Method::Post, "/ingest") => handle_ingest(vault_path, passphrase, body),
        (Method::Post, "/write") => handle_write(vault_path, passphrase, body),
        (Method::Post, "/classify") => handle_classify(vault_path, passphrase, body),
        (Method::Get, "/") => (
            StatusCode(200),
            serde_json::json!({
                "service": "p3ak-vault",
                "routes": [
                    "GET  /health",
                    "GET  /search?q=...&mode=hybrid&limit=5&store=FN&room=work",
                    "GET  /docs",
                    "GET  /wal",
                    "POST /ingest   {\"file\":\"/abs/path\",\"room\":\"work\"}",
                    "POST /write    {\"type\":\"goal\",\"payload\":{...}}",
                    "POST /classify {\"only_missing\":true}",
                ]
            }),
        ),
        _ => (
            StatusCode(404),
            serde_json::json!({"error": "not found", "url": url}),
        ),
    }
}

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

fn handle_health(vault_path: &Path, passphrase: Option<&str>) -> (StatusCode, serde_json::Value) {
    match crate::vault_load(vault_path, passphrase) {
        Ok(v) => (
            StatusCode(200),
            serde_json::json!({
                "ok": true,
                "vault": vault_path.display().to_string(),
                "docs": v.docs.iter().filter(|d| !d.deleted).count(),
                "encrypted": passphrase.is_some(),
            }),
        ),
        Err(e) => (
            StatusCode(503),
            serde_json::json!({"ok": false, "error": e.to_string()}),
        ),
    }
}

fn handle_search(
    vault_path: &Path,
    passphrase: Option<&str>,
    query_str: &str,
) -> (StatusCode, serde_json::Value) {
    let params = parse_query(query_str);
    let q = match params.get("q").or_else(|| params.get("query")) {
        Some(v) => v.clone(),
        None => {
            return (
                StatusCode(400),
                serde_json::json!({"error": "missing required param 'q'"}),
            )
        }
    };
    let mode_str = params.get("mode").map(|s| s.as_str()).unwrap_or("hybrid");
    let limit: usize = params
        .get("limit")
        .and_then(|s| s.parse().ok())
        .unwrap_or(5);
    let store = params.get("store").cloned();
    let room = params.get("room").cloned();

    let mode = parse_mode(mode_str);
    let vault = match crate::vault_load(vault_path, passphrase) {
        Ok(v) => v,
        Err(e) => return (StatusCode(503), serde_json::json!({"error": e.to_string()})),
    };

    let index_path = std::path::PathBuf::from(format!("{}.idx", vault_path.display()));

    let results = run_search(
        &vault,
        vault_path,
        &index_path,
        &q,
        limit,
        mode,
        store.as_deref(),
        room.as_deref(),
    );
    match results {
        Ok(r) => (StatusCode(200), serde_json::Value::Array(r)),
        Err(e) => (StatusCode(500), serde_json::json!({"error": e.to_string()})),
    }
}

fn handle_read_docs(
    vault_path: &Path,
    passphrase: Option<&str>,
) -> (StatusCode, serde_json::Value) {
    match crate::vault_load(vault_path, passphrase) {
        Ok(v) => {
            let docs: Vec<&vault_core::types::Doc> = v.docs.iter().filter(|d| !d.deleted).collect();
            (
                StatusCode(200),
                serde_json::to_value(&docs).unwrap_or(serde_json::Value::Array(vec![])),
            )
        }
        Err(e) => (StatusCode(503), serde_json::json!({"error": e.to_string()})),
    }
}

fn handle_read_wal(vault_path: &Path, passphrase: Option<&str>) -> (StatusCode, serde_json::Value) {
    match crate::vault_load(vault_path, passphrase) {
        Ok(v) => (
            StatusCode(200),
            serde_json::to_value(&v.wal).unwrap_or(serde_json::Value::Array(vec![])),
        ),
        Err(e) => (StatusCode(503), serde_json::json!({"error": e.to_string()})),
    }
}

fn handle_ingest(
    vault_path: &Path,
    passphrase: Option<&str>,
    body: &str,
) -> (StatusCode, serde_json::Value) {
    let params: serde_json::Value = match serde_json::from_str(body) {
        Ok(v) => v,
        Err(e) => {
            return (
                StatusCode(400),
                serde_json::json!({"error": format!("invalid JSON: {e}")}),
            )
        }
    };
    let file_str = match params.get("file").and_then(|v| v.as_str()) {
        Some(f) => f.to_string(),
        None => {
            return (
                StatusCode(400),
                serde_json::json!({"error": "missing 'file' field"}),
            )
        }
    };
    let room = params
        .get("room")
        .and_then(|v| v.as_str())
        .unwrap_or("default")
        .to_string();

    let file_path = std::path::Path::new(&file_str);
    let mut vault = match crate::vault_load(vault_path, passphrase) {
        Ok(v) => v,
        Err(e) => return (StatusCode(503), serde_json::json!({"error": e.to_string()})),
    };

    match vault_core::ingest::ingest_file(&mut vault, file_path, &room) {
        Ok(report) => {
            if report.action != vault_core::ingest::IngestAction::Skipped {
                if let Err(e) = crate::vault_save(&vault, vault_path, passphrase) {
                    return (
                        StatusCode(500),
                        serde_json::json!({"error": format!("save failed: {e}")}),
                    );
                }
                vault_core::zvec::invalidate_sidecar(vault_path);
            }
            (
                StatusCode(200),
                serde_json::json!({
                    "ok": true,
                    "doc_id": report.doc_id,
                    "action": format!("{:?}", report.action).to_lowercase(),
                    "source_format": report.source_format,
                }),
            )
        }
        Err(e) => (StatusCode(500), serde_json::json!({"error": e.to_string()})),
    }
}

fn handle_write(
    vault_path: &Path,
    passphrase: Option<&str>,
    body: &str,
) -> (StatusCode, serde_json::Value) {
    let params: serde_json::Value = match serde_json::from_str(body) {
        Ok(v) => v,
        Err(e) => {
            return (
                StatusCode(400),
                serde_json::json!({"error": format!("invalid JSON: {e}")}),
            )
        }
    };
    let action_type = match params.get("type").and_then(|v| v.as_str()) {
        Some(t) if matches!(t, "goal" | "doc" | "plan" | "review") => {
            format!("write_{t}")
        }
        Some(other) => {
            return (
                StatusCode(400),
                serde_json::json!({"error": format!("unknown type '{other}'; use goal|doc|plan|review")}),
            )
        }
        None => {
            return (
                StatusCode(400),
                serde_json::json!({"error": "missing 'type' field"}),
            )
        }
    };
    let payload = match params.get("payload") {
        Some(p) => p.clone(),
        None => {
            return (
                StatusCode(400),
                serde_json::json!({"error": "missing 'payload' field"}),
            )
        }
    };

    let mut vault = match crate::vault_load(vault_path, passphrase) {
        Ok(v) => v,
        Err(e) => return (StatusCode(503), serde_json::json!({"error": e.to_string()})),
    };

    let entry_id = vault_core::wal::append_entry(&mut vault, &action_type, Some(payload));
    if let Err(e) = crate::vault_save(&vault, vault_path, passphrase) {
        return (
            StatusCode(500),
            serde_json::json!({"error": format!("save failed: {e}")}),
        );
    }
    (
        StatusCode(200),
        serde_json::json!({"ok": true, "entry_id": entry_id}),
    )
}

fn handle_classify(
    vault_path: &Path,
    passphrase: Option<&str>,
    body: &str,
) -> (StatusCode, serde_json::Value) {
    let params: serde_json::Value =
        serde_json::from_str(body).unwrap_or(serde_json::Value::Object(Default::default()));
    let only_missing = params
        .get("only_missing")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);

    let mut vault = match crate::vault_load(vault_path, passphrase) {
        Ok(v) => v,
        Err(e) => return (StatusCode(503), serde_json::json!({"error": e.to_string()})),
    };

    let mut classified = 0usize;
    let mut skipped = 0usize;

    for doc in &mut vault.docs {
        if only_missing && doc.classification.is_some() {
            skipped += 1;
            continue;
        }
        let title_stem = std::path::Path::new(&doc.filename)
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or(&doc.filename)
            .replace(['-', '_'], " ");
        let results = vault_core::classify::classify_document(&title_stem, &doc.normalized_text);
        if let Some(primary) = results.into_iter().next() {
            doc.classification = Some(primary);
            classified += 1;
        } else {
            skipped += 1;
        }
    }

    if let Err(e) = crate::vault_save(&vault, vault_path, passphrase) {
        return (
            StatusCode(500),
            serde_json::json!({"error": format!("save failed: {e}")}),
        );
    }
    (
        StatusCode(200),
        serde_json::json!({"ok": true, "classified": classified, "skipped": skipped}),
    )
}

// ─── Search helper ────────────────────────────────────────────────────────────

enum ServeMode {
    Flat,
    Pageindex,
    Hybrid,
}

fn parse_mode(s: &str) -> ServeMode {
    match s.to_lowercase().as_str() {
        "pageindex" => ServeMode::Pageindex,
        "hybrid" => ServeMode::Hybrid,
        _ => ServeMode::Flat,
    }
}

#[allow(clippy::too_many_arguments)]
fn run_search(
    vault: &vault_core::types::Vault,
    vault_path: &Path,
    index_path: &Path,
    query: &str,
    limit: usize,
    mode: ServeMode,
    store: Option<&str>,
    room: Option<&str>,
) -> anyhow::Result<Vec<serde_json::Value>> {
    use vault_core::search;
    use vault_core::types::StoreId;

    let store_filter: Option<StoreId> = store
        .map(|s| StoreId::from_str(s).ok_or_else(|| anyhow::anyhow!("unknown store '{s}'")))
        .transpose()?;
    let room_filter: Option<String> = room
        .filter(|r| !r.trim().is_empty())
        .map(|r| r.trim().to_string());

    let doc_meta: std::collections::HashMap<u64, (&str, &str, Option<String>)> = vault
        .docs
        .iter()
        .map(|d| {
            let store_label = d
                .classification
                .as_ref()
                .map(|c| c.store_id.as_str().to_string());
            (
                d.doc_id,
                (d.filename.as_str(), d.room.as_str(), store_label),
            )
        })
        .collect();

    let raw = match mode {
        ServeMode::Flat => {
            search::build_index(vault, index_path)?;
            let mut r = search::search(index_path, query, limit)?;
            if let Some(ref sid) = store_filter {
                let allowed = search::store_filtered_doc_ids(vault, sid);
                r.retain(|x| allowed.contains(&x.doc_id));
            }
            if let Some(ref rm) = room_filter {
                let allowed = search::room_filtered_doc_ids(vault, rm);
                r.retain(|x| allowed.contains(&x.doc_id));
            }
            r.into_iter()
                .map(|r| serde_json::to_value(r).unwrap())
                .collect::<Vec<_>>()
        }
        ServeMode::Pageindex => {
            let cfg = search::PageIndexSearchConfig {
                store_filter,
                room_filter,
                ..Default::default()
            };
            search::pageindex_search(vault, index_path, query, limit, &cfg)?
                .into_iter()
                .map(|r| serde_json::to_value(r).unwrap())
                .collect()
        }
        ServeMode::Hybrid => {
            let vocab = vault_core::zvec::get_or_build_vocab(vault, vault_path);
            let cfg = search::HybridSearchConfig {
                store_filter,
                room_filter,
                ..Default::default()
            };
            search::hybrid_search_with_vocab(vault, index_path, query, limit, &cfg, &vocab)?
                .into_iter()
                .map(|r| serde_json::to_value(r).unwrap())
                .collect()
        }
    };

    let enriched = raw
        .into_iter()
        .map(|mut v| {
            if let Some(doc_id) = v.get("doc_id").and_then(|x| x.as_u64()) {
                if let Some(&(filename, room_str, ref store_label)) = doc_meta.get(&doc_id) {
                    let obj = v.as_object_mut().unwrap();
                    obj.insert("filename".into(), serde_json::json!(filename));
                    obj.insert("room".into(), serde_json::json!(room_str));
                    if let Some(lbl) = store_label {
                        obj.insert("store".into(), serde_json::json!(lbl));
                    }
                }
            }
            v
        })
        .collect();

    Ok(enriched)
}

// ─── Utilities ────────────────────────────────────────────────────────────────

/// Parse a URL query string into a key→value map.
fn parse_query(qs: &str) -> std::collections::HashMap<String, String> {
    qs.split('&')
        .filter_map(|part| {
            let mut kv = part.splitn(2, '=');
            let key = url_decode(kv.next()?);
            let val = url_decode(kv.next().unwrap_or(""));
            Some((key, val))
        })
        .collect()
}

/// Minimal percent-decoding for query string values.
fn url_decode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let mut chars = s.chars().peekable();
    while let Some(c) = chars.next() {
        if c == '%' {
            let h1 = chars.next().unwrap_or('0');
            let h2 = chars.next().unwrap_or('0');
            if let Ok(byte) = u8::from_str_radix(&format!("{h1}{h2}"), 16) {
                out.push(byte as char);
            } else {
                out.push('%');
                out.push(h1);
                out.push(h2);
            }
        } else if c == '+' {
            out.push(' ');
        } else {
            out.push(c);
        }
    }
    out
}

/// Send a JSON response and consume the request.
fn send_response(req: Request, status: StatusCode, body: serde_json::Value) -> anyhow::Result<()> {
    let body_str = serde_json::to_string_pretty(&body)?;
    let content_type: Header = "Content-Type: application/json".parse().unwrap();
    let response = Response::from_string(body_str)
        .with_status_code(status)
        .with_header(content_type);
    req.respond(response)
        .map_err(|e| anyhow::anyhow!("failed to send response: {e}"))?;
    Ok(())
}
