//! Library surface for relicario-server, exposing pure helpers used by the
//! pre-receive hooks so they can be unit-tested.

/// Classification of a single changed path inside an org repo.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathClass {
    /// `members.json`, `collections.json`, `org.json` — only Owner/Admin may write.
    Protected,
    /// `items/<slug>/<id>.enc` and `attachments/<slug>/<item-id>/<att-id>.enc` —
    /// writer must hold a grant for `<slug>`.
    Item { collection: String },
    /// `keys/<id>.enc`, `manifest.enc`, `.gitignore`, etc. — gated only by the
    /// per-commit signature check (signer must be a current member).
    Unrestricted,
    /// Structurally invalid path; commit must be rejected.
    Rejected(String),
}

/// Classify a repo-relative path. Pure; no I/O.
pub fn classify_path(path: &str) -> PathClass {
    match path {
        "members.json" | "collections.json" | "org.json" => return PathClass::Protected,
        _ => {}
    }

    if let Some(rest) = path.strip_prefix("items/") {
        // Expect exactly: <slug>/<id>.enc  → two segments after the prefix.
        let segments: Vec<&str> = rest.split('/').collect();
        if segments.len() != 2 {
            return PathClass::Rejected("items path must be items/<slug>/<id>.enc".to_string());
        }
        let slug = segments[0];
        if slug.is_empty() {
            return PathClass::Rejected("empty collection slug in items path".to_string());
        }
        // Defense-in-depth: mirror `OrgCollections::validate` — a slug containing
        // '.' (e.g. a `..`/`.` path-traversal attempt) is structurally invalid.
        // git normalizes most `./` away before the hook sees the path, so this is
        // unreachable today; it keeps the hook self-defensive regardless.
        if slug.contains('.') {
            return PathClass::Rejected(format!("invalid collection slug: {:?}", slug));
        }
        return PathClass::Item { collection: slug.to_string() };
    }

    if let Some(rest) = path.strip_prefix("attachments/") {
        // Expect exactly: <slug>/<item-id>/<att-id>.enc → three segments.
        let segments: Vec<&str> = rest.split('/').collect();
        if segments.len() != 3 {
            return PathClass::Rejected(
                "attachments path must be attachments/<slug>/<item-id>/<att-id>.enc".to_string());
        }
        let slug = segments[0];
        if slug.is_empty() {
            return PathClass::Rejected("empty collection slug in attachments path".to_string());
        }
        if slug.contains('.') {
            return PathClass::Rejected(format!("invalid collection slug: {:?}", slug));
        }
        return PathClass::Item { collection: slug.to_string() };
    }

    PathClass::Unrestricted
}

/// Extract the `schema_version` field from any org JSON document.
/// Returns an error if the field is absent or not a u32.
pub fn extract_schema_version(json: &str) -> Result<u32, String> {
    let value: serde_json::Value =
        serde_json::from_str(json).map_err(|e| format!("parse json: {e}"))?;
    value
        .get("schema_version")
        .and_then(|v| v.as_u64())
        .map(|n| n as u32)
        .ok_or_else(|| "missing or non-integer schema_version".to_string())
}
