import pathlib

p = pathlib.Path("crates/spt-daemon/src/webserve.rs"); s = p.read_text(encoding="utf-8")


def rep(old, new, n=1):
    global s
    assert s.count(old) == n, (old[:80], s.count(old))
    s = s.replace(old, new)


rep('''//! Node-prefixed W0 HTTP routing over the serving registry.
//!
//! The listener remains in `docshost`; docs bytes and directory containment use
//! its existing sanitizer and serving implementation. Registry state is loaded
//! from the explicitly supplied home on every index/file request.

use std::fmt::Write as _;
use std::path::{Component, Path};
''', '''//! Node-prefixed HTTP routing over the serving registry (W0), and the
//! resolve-before-bytes split the cross-node proxy rides (W1).
//!
//! The listener remains in `docshost`; docs bytes and directory containment use
//! its existing sanitizer and containment gates. Registry state is loaded from
//! the explicitly supplied home on every index/file request.
//!
//! ## Resolve, then materialize (W1)
//!
//! [`resolve_path`] decides WHAT a request names without moving a byte: a
//! finished response ([`Resolved::Ready`] — redirects, errors, the index, the
//! docs compatibility surface), a file on this node ([`Resolved::File`]), or a
//! resource a KNOWN subnet peer owns ([`Resolved::Proxy`]). The local listener
//! materializes the first two itself and hands the third to the daemon's
//! proxy; the OWNING node's dispatcher runs the same `resolve_path` for a
//! request that arrived over the wire and streams a `File` plan back in
//! chunks. One router, both sides — which is what keeps a served name
//! meaning the same thing whichever box the URL was pasted into (ADR-0056).

use std::fmt::Write as _;
use std::path::{Component, Path, PathBuf};
''')

rep('''use crate::docshost::{content_type_for, sanitize_request_path, serve_path};
''', '''use crate::docshost::{content_type_for, locate_path, sanitize_request_path, serve_path};

/// What one GET/HEAD path names, decided before any bytes move.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub enum Resolved {
    /// A complete response: redirects, refusals, the index page, the docs
    /// compatibility surface and its 404s.
    Ready(Response<Full<Bytes>>),
    /// A file on THIS node — read whole by the local listener, streamed in
    /// bounded chunks by the owner-side proxy serve.
    File {
        path: PathBuf,
        content_type: &'static str,
    },
    /// A resource a KNOWN subnet peer owns (`node_hex` is its Ed25519 node id,
    /// `node_label` the label the URL used): the daemon proxies the request.
    Proxy {
        node_label: String,
        node_hex: String,
    },
}

/// How a `Range` header applies to a body of `len` bytes.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RangePlan {
    /// No usable range: answer the whole body as 200 (a malformed or
    /// multi-part range is ignored, which HTTP permits).
    Full,
    /// Bytes `start..=end` as 206 with `Content-Range: bytes start-end/len`.
    Partial { start: u64, end: u64 },
    /// The range starts past the end: 416 with `Content-Range: bytes */len`.
    Unsatisfiable,
}

/// ONE range grammar for the loopback listener and the cross-node owner
/// (ADR-0056: the same URL answers the same way on every box): `bytes=a-b`,
/// `bytes=a-` and the suffix form `bytes=-n`. A closed end past the last byte
/// is clamped; a start past the last byte is unsatisfiable; anything else
/// (no header, another unit, several ranges, a reversed range, garbage) is
/// ignored and the whole body is served as 200.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub fn apply_range(len: u64, range: Option<&str>) -> RangePlan {
    let Some(spec) = range.and_then(|r| r.trim().strip_prefix("bytes=")) else {
        return RangePlan::Full;
    };
    let spec = spec.trim();
    if spec.contains(',') {
        return RangePlan::Full;
    }
    let Some((first, last)) = spec.split_once('-') else {
        return RangePlan::Full;
    };
    let (first, last) = (first.trim(), last.trim());
    if first.is_empty() {
        // Suffix form: the last `n` bytes.
        let Ok(n) = last.parse::<u64>() else {
            return RangePlan::Full;
        };
        if n == 0 || len == 0 {
            return RangePlan::Unsatisfiable;
        }
        let start = len.saturating_sub(n);
        return RangePlan::Partial { start, end: len - 1 };
    }
    let Ok(start) = first.parse::<u64>() else {
        return RangePlan::Full;
    };
    if start >= len {
        return RangePlan::Unsatisfiable;
    }
    let end = if last.is_empty() {
        len - 1
    } else {
        let Ok(end) = last.parse::<u64>() else {
            return RangePlan::Full;
        };
        if end < start {
            return RangePlan::Full;
        }
        end.min(len - 1)
    };
    RangePlan::Partial { start, end }
}

/// The head of a file response after the range is applied: what to say and
/// which bytes to send. Shared by the local read and the owner-side stream.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilePlan {
    pub status: StatusCode,
    pub content_type: &'static str,
    /// Byte count of the body to send (0 for a 416).
    pub content_length: u64,
    /// The `Content-Range` header value, when one applies.
    pub content_range: Option<String>,
    /// First byte to send.
    pub start: u64,
}

impl FilePlan {
    /// The response headers, in wire form (also the shape the owner carries
    /// back across the stream verbatim).
    pub fn headers(&self) -> Vec<(String, String)> {
        let mut headers = vec![
            ("content-type".to_owned(), self.content_type.to_owned()),
            ("content-length".to_owned(), self.content_length.to_string()),
        ];
        if let Some(range) = &self.content_range {
            headers.push(("content-range".to_owned(), range.clone()));
        }
        headers
    }
}

// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub fn plan_file(len: u64, content_type: &'static str, range: Option<&str>) -> FilePlan {
    match apply_range(len, range) {
        RangePlan::Full => FilePlan {
            status: StatusCode::OK,
            content_type,
            content_length: len,
            content_range: None,
            start: 0,
        },
        RangePlan::Partial { start, end } => FilePlan {
            status: StatusCode::PARTIAL_CONTENT,
            content_type,
            content_length: end - start + 1,
            content_range: Some(format!("bytes {start}-{end}/{len}")),
            start,
        },
        RangePlan::Unsatisfiable => FilePlan {
            status: StatusCode::RANGE_NOT_SATISFIABLE,
            content_type: "text/plain; charset=utf-8",
            content_length: 0,
            content_range: Some(format!("bytes */{len}")),
            start: 0,
        },
    }
}

/// The 502 every unreachable-owner path answers with: it names the NODE,
/// never a surface (the surface belongs to the owner's 403), and says why.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub fn node_unavailable(node_label: &str, why: &str, head_only: bool) -> Response<Full<Bytes>> {
    text(StatusCode::BAD_GATEWAY, format!("NODE_UNAVAILABLE: {node_label}: {why}\\n"), head_only)
}

/// Read a resolved file plan into a complete response (the local listener's
/// leg; the owner-side proxy streams the same plan instead). A file that
/// vanished between resolve and read is the 404 its name deserves.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
fn serve_file(path: &Path, content_type: &'static str, head_only: bool, range: Option<&str>) -> Response<Full<Bytes>> {
    use std::io::{Read as _, Seek as _, SeekFrom};
    let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
    let mut file = match std::fs::File::open(path) {
        Ok(file) => file,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            return text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only)
        }
        Err(error) => return text(StatusCode::INTERNAL_SERVER_ERROR, format!("READ_FAIL: {error}\\n"), head_only),
    };
    let len = match file.metadata() {
        Ok(meta) => meta.len(),
        Err(error) => return text(StatusCode::INTERNAL_SERVER_ERROR, format!("READ_FAIL: {error}\\n"), head_only),
    };
    let plan = plan_file(len, content_type, range);
    let mut builder = Response::builder().status(plan.status);
    for (name, value) in plan.headers() {
        builder = builder.header(name, value);
    }
    let body = if head_only || plan.content_length == 0 {
        Vec::new()
    } else {
        let mut bytes = vec![0u8; plan.content_length as usize];
        if let Err(error) = file.seek(SeekFrom::Start(plan.start)).and_then(|_| file.read_exact(&mut bytes)) {
            return text(StatusCode::INTERNAL_SERVER_ERROR, format!("READ_FAIL: {error}\\n"), head_only);
        }
        bytes
    };
    builder.body(Full::new(Bytes::from(body))).expect("file response builds")
}

/// Turn a resolution into the local listener's response. `Proxy` here means
/// the caller has no daemon to proxy through (a bare listener, the unit
/// tests): the honest answer is the 502 naming the node.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub fn materialize(resolved: Resolved, head_only: bool, range: Option<&str>) -> Response<Full<Bytes>> {
    match resolved {
        Resolved::Ready(response) => response,
        Resolved::File { path, content_type } => serve_file(&path, content_type, head_only, range),
        Resolved::Proxy { node_label, .. } => {
            node_unavailable(&node_label, "this listener has no daemon to proxy through", head_only)
        }
    }
}

/// A docs-root-contained file as a plan, or the sanitizer's/containment's
/// finished refusal.
fn planned(root: &Path, uri_path: &str, head_only: bool) -> Resolved {
    match locate_path(root, uri_path, head_only) {
        Ok((path, content_type)) => Resolved::File { path, content_type },
        Err(response) => Resolved::Ready(response),
    }
}
''')

# known_subnet_node -> hex
rep('''/// Match node labels, not endpoint IDs. Membership is loaded for this home on
/// every request so joining/leaving a subnet changes only the compatibility
/// alias's shadow, never the canonical local docs URL.
fn is_known_subnet_node(home: &Path, label: &str) -> bool {
    let identity = home.join("identity");
    let subnets = spt_store::subnet::SubnetStore::load_from(&identity.join("subnet.json"));
    let roster = spt_store::roster::RosterStore::load_from(&identity.join("roster.json"));
    subnets.subnets.iter().any(|subnet| {
        let path = crate::registryhost::RegistryHost::snapshot_path(&identity.join("registry"), &subnet.name);
        let registry: spt_net::net::registry::SubnetRegistry = std::fs::read(path)
            .ok()
            .and_then(|bytes| serde_json::from_slice(&bytes).ok())
            .unwrap_or_default();
        roster.members_in(&subnet.name).any(|member| {
            let advertised = registry.node_labels()
                .find_map(|(node, label)| (node == member.pubkey_hex).then_some(label))
                .unwrap_or(&member.label);
            advertised.eq_ignore_ascii_case(label)
        })
    })
}
''', '''/// Match node labels, not endpoint IDs, and answer the member's node id (the
/// Ed25519 pubkey hex the proxy dials). Membership is loaded for this home on
/// every request so joining/leaving a subnet changes only the compatibility
/// alias's shadow, never the canonical local docs URL. A registry-advertised
/// label wins over the roster's; two members carrying one label resolve to
/// the first match in subnet-then-roster order.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub fn known_subnet_node(home: &Path, label: &str) -> Option<String> {
    let identity = home.join("identity");
    let subnets = spt_store::subnet::SubnetStore::load_from(&identity.join("subnet.json"));
    let roster = spt_store::roster::RosterStore::load_from(&identity.join("roster.json"));
    subnets.subnets.iter().find_map(|subnet| {
        let path = crate::registryhost::RegistryHost::snapshot_path(&identity.join("registry"), &subnet.name);
        let registry: spt_net::net::registry::SubnetRegistry = std::fs::read(path)
            .ok()
            .and_then(|bytes| serde_json::from_slice(&bytes).ok())
            .unwrap_or_default();
        roster.members_in(&subnet.name).find_map(|member| {
            let advertised = registry.node_labels()
                .find_map(|(node, label)| (node == member.pubkey_hex).then_some(label))
                .unwrap_or(&member.label);
            advertised.eq_ignore_ascii_case(label).then(|| member.pubkey_hex.clone())
        })
    })
}
''')

# reference -> Resolved
rep('''// [impl->REQ-WEB-SERVING-REGISTRY]
fn reference(
    home: &Path,
    registry: &ServingRegistry,
    remainder: &str,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Response<Full<Bytes>> {
    let (raw_name, subpath) = remainder.split_once('/').unwrap_or((remainder, ""));
    let Some(name) = route_segment(raw_name) else {
        return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one served-name segment\\n".to_owned(), head_only);
    };
    // HTTP names are names, not management IDs. An ID accepted by `serve rm`
    // must not turn into an unlisted second URL (or shadow another entry).
    let Some(entry) = registry.entries().find(|entry| entry.served_name == name) else {
        return text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only);
    };
    serve_entry(home, entry, subpath, uri_path, query, head_only)
}
''', '''// [impl->REQ-WEB-SERVING-REGISTRY]
fn reference(
    home: &Path,
    registry: &ServingRegistry,
    remainder: &str,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Resolved {
    let (raw_name, subpath) = remainder.split_once('/').unwrap_or((remainder, ""));
    let Some(name) = route_segment(raw_name) else {
        return Resolved::Ready(text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one served-name segment\\n".to_owned(), head_only));
    };
    // HTTP names are names, not management IDs. An ID accepted by `serve rm`
    // must not turn into an unlisted second URL (or shadow another entry).
    let Some(entry) = registry.entries().find(|entry| entry.served_name == name) else {
        return Resolved::Ready(text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only));
    };
    serve_entry(home, entry, subpath, uri_path, query, head_only)
}
''')

# serve_entry -> Resolved
rep('''// [impl->REQ-WEB-SERVING-REGISTRY]
fn serve_entry(
    home: &Path,
    entry: &ServedEntry,
    subpath: &str,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Response<Full<Bytes>> {
    let name = &entry.served_name;
    if let Some(adapter) = &entry.adapter {
        let valid_root = spt_store::perch::validate_adapter_web_dir_in(home, adapter);
        if !matches!((valid_root, entry.path.canonicalize()), (Ok(expected), Ok(actual)) if expected == actual) {
            return text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only);
        }
    }
    match entry.kind {
        ServedKind::Dir if entry.path.is_dir() => {
            // The root index must be addressed as a directory so relative links
            // stay below the served root. Docs compatibility paths are untouched.
            if subpath.is_empty() && !uri_path.ends_with('/') {
                return redirect_directory(uri_path, query);
            }
            serve_path(&entry.path, subpath, head_only)
        }
        ServedKind::File if subpath.is_empty() && entry.path.is_file() => {
            match std::fs::read(&entry.path) {
                Ok(bytes) => response(StatusCode::OK, content_type_for(&entry.path), bytes, head_only),
                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
                    text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only)
                }
                Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, format!("READ_FAIL: {error}\\n"), head_only),
            }
        }
        // Attachment byte serving belongs to W2, even if an entry of that
        // kind already exists in the forward-compatible registry format.
        _ => text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only),
    }
}
''', '''// [impl->REQ-WEB-SERVING-REGISTRY]
fn serve_entry(
    home: &Path,
    entry: &ServedEntry,
    subpath: &str,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Resolved {
    let name = &entry.served_name;
    if let Some(adapter) = &entry.adapter {
        let valid_root = spt_store::perch::validate_adapter_web_dir_in(home, adapter);
        if !matches!((valid_root, entry.path.canonicalize()), (Ok(expected), Ok(actual)) if expected == actual) {
            return Resolved::Ready(text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only));
        }
    }
    match entry.kind {
        ServedKind::Dir if entry.path.is_dir() => {
            // The root index must be addressed as a directory so relative links
            // stay below the served root. Docs compatibility paths are untouched.
            if subpath.is_empty() && !uri_path.ends_with('/') {
                return Resolved::Ready(redirect_directory(uri_path, query));
            }
            planned(&entry.path, subpath, head_only)
        }
        // Resolved at request time (ADR-0057): the plan names the CURRENT
        // file, so an edit is visible and a deletion is the 404 below.
        ServedKind::File if subpath.is_empty() && entry.path.is_file() => Resolved::File {
            path: entry.path.clone(),
            content_type: content_type_for(&entry.path),
        },
        // Attachment byte serving belongs to W2, even if an entry of that
        // kind already exists in the forward-compatible registry format.
        _ => Resolved::Ready(text(StatusCode::NOT_FOUND, format!("NOT_FOUND: served resource {name}\\n"), head_only)),
    }
}
''')

# adapter -> Resolved
rep('''// [impl->REQ-WEB-SERVING-REGISTRY]
fn adapter(
    home: &Path,
    registry: &ServingRegistry,
    remainder: &str,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Response<Full<Bytes>> {
    let (raw_adapter, subpath) = remainder.split_once('/').unwrap_or((remainder, ""));
    let Some(name) = route_segment(raw_adapter) else {
        return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one adapter segment\\n".to_owned(), head_only);
    };
    match registry.entries().find(|entry| entry.adapter.as_deref() == Some(name.as_str())) {
        Some(entry) => serve_entry(home, entry, subpath, uri_path, query, head_only),
        None => text(StatusCode::NOT_FOUND, format!("NOT_FOUND: facet a adapter {name}\\n"), head_only),
    }
}

/// Resolve one GET/HEAD path. The production listener owns method selection;
/// this module owns only routing and never reads the process's SPT_HOME.
// [impl->REQ-WEB-URL-NODE-PREFIX]
pub fn handle_path(
    home: &Path,
    docs_root: &Path,
    local_node: &str,
    port: u16,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Response<Full<Bytes>> {
    if uri_path == "/" {
        return redirect_node(local_node, query);
    }
    let Some(relative) = sanitize_request_path(uri_path) else {
        let raw_first = uri_path.trim_start_matches('/').split('/').next().unwrap_or("");
        if route_segment(raw_first).is_some_and(|first| {
            first.eq_ignore_ascii_case(local_node)
                && !spt_store::hostlabel::is_reserved_web_facet(&first)
        }) {
            return text(StatusCode::BAD_REQUEST, "BAD_PATH: rejected by the docs-root sanitizer\\n".to_owned(), head_only);
        }
        // Preserve the docs-less compatibility response as well as the
        // installed bundle's sanitizer status; no bytes bypass containment.
        return serve_path(docs_root, uri_path, head_only);
    };
    // ADR-0056 Amendment 1: local node, exact docs root-file leaf, known peer,
    // then the unchanged docs compatibility surface (including its docs 404).
    let first = relative.components().next().and_then(|component| match component {
        Component::Normal(first) => first.to_str(),
        _ => None,
    });
    let Some(first) = first else {
        return serve_path(docs_root, uri_path, head_only);
    };
    if spt_store::hostlabel::is_reserved_web_facet(first) {
        return serve_path(docs_root, uri_path, head_only);
    }
    let path = uri_path.trim_start_matches('/');
    let bare_node = !path.contains('/') && route_segment(path).is_some();
    if !first.eq_ignore_ascii_case(local_node) {
        // Rule 2.5 protects root files by URL shape, not hostname policy.
        // A trailing slash or child segment always keeps the node grammar.
        if bare_node && docs_root.join(first).is_file() {
            return serve_path(docs_root, uri_path, head_only);
        }
        if is_known_subnet_node(home, first) {
            if bare_node {
                return redirect_node(first, query);
            }
            return text(StatusCode::BAD_GATEWAY, format!("NODE_UNAVAILABLE: {first}: cross-node serving is not available yet\\n"), head_only);
        }
        return serve_path(docs_root, uri_path, head_only);
    }
    if bare_node {
        return redirect_node(local_node, query);
    }
    let (raw_node, remainder) = path.split_once('/').unwrap_or((path, ""));
    let Some(_) = route_segment(raw_node) else {
        return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one node segment\\n".to_owned(), head_only);
    };
    if remainder.is_empty() {
        return match registry_at(home) {
            Ok(registry) => index(&registry, local_node, port, query, head_only),
            Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only),
        };
    }
    let (raw_facet, remainder) = remainder.split_once('/').unwrap_or((remainder, ""));
    let Some(facet) = route_segment(raw_facet) else {
        return text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one facet segment\\n".to_owned(), head_only);
    };
    match facet.as_str() {
        "docs" => {
            if remainder.is_empty() && !uri_path.ends_with('/') {
                return redirect_directory(uri_path, query);
            }
            serve_path(docs_root, remainder, head_only)
        }
        "f" if !remainder.is_empty() => match registry_at(home) {
            Ok(registry) => reference(home, &registry, remainder, uri_path, query, head_only),
            Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only),
        },
        "a" if !remainder.is_empty() => match registry_at(home) {
            Ok(registry) => adapter(home, &registry, remainder, uri_path, query, head_only),
            Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only),
        },
        "m" | "bin" | "install" => text(StatusCode::NOT_FOUND, format!("FACET_UNAVAILABLE: {facet}\\n"), head_only),
        "a" | "f" => text(StatusCode::NOT_FOUND, format!("FACET_NOT_FOUND: {facet}\\n"), head_only),
        _ => match registry_at(home) {
            Ok(registry) => {
                let entry = registry.entries().find(|entry| entry.short_alias && entry.served_name == facet);
                match entry {
                    Some(entry) => serve_entry(home, entry, remainder, uri_path, query, head_only),
                    None => text(StatusCode::NOT_FOUND, format!("FACET_NOT_FOUND: {facet}\\n"), head_only),
                }
            }
            Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only),
        },
    }
}
''', '''// [impl->REQ-WEB-SERVING-REGISTRY]
fn adapter(
    home: &Path,
    registry: &ServingRegistry,
    remainder: &str,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Resolved {
    let (raw_adapter, subpath) = remainder.split_once('/').unwrap_or((remainder, ""));
    let Some(name) = route_segment(raw_adapter) else {
        return Resolved::Ready(text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one adapter segment\\n".to_owned(), head_only));
    };
    match registry.entries().find(|entry| entry.adapter.as_deref() == Some(name.as_str())) {
        Some(entry) => serve_entry(home, entry, subpath, uri_path, query, head_only),
        None => Resolved::Ready(text(StatusCode::NOT_FOUND, format!("NOT_FOUND: facet a adapter {name}\\n"), head_only)),
    }
}

/// The peer arm of the router: which facets of `/<peer>/...` open a stream to
/// the owner, and which the requester answers itself. Reserved facets stay
/// router-first on BOTH boxes (W0 reserved them before any registry lookup),
/// so an unbuilt facet is refused here without a hop, and a bare `f`/`a`
/// (no name) is the same local FACET_NOT_FOUND the local node answers.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
fn peer_arm(node_label: &str, node_hex: String, remainder: &str, head_only: bool) -> Resolved {
    let proxy = || Resolved::Proxy { node_label: node_label.to_owned(), node_hex: node_hex.clone() };
    if remainder.is_empty() {
        return proxy(); // the peer's index page (and its ?json twin)
    }
    let (raw_facet, rest) = remainder.split_once('/').unwrap_or((remainder, ""));
    let Some(facet) = route_segment(raw_facet) else {
        return Resolved::Ready(text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one facet segment\\n".to_owned(), head_only));
    };
    match facet.as_str() {
        "docs" => proxy(),
        "f" | "a" if !rest.is_empty() => proxy(),
        "m" | "bin" | "install" => Resolved::Ready(text(StatusCode::NOT_FOUND, format!("FACET_UNAVAILABLE: {facet}\\n"), head_only)),
        "a" | "f" => Resolved::Ready(text(StatusCode::NOT_FOUND, format!("FACET_NOT_FOUND: {facet}\\n"), head_only)),
        _ => proxy(), // a short alias only the owner's registry can resolve
    }
}

/// The registering endpoint of the served entry a node-prefixed path names on
/// THIS node — the WEB gate's subject on the owner side. `None` when the path
/// names no registry entry (the index, the docs facet, a miss): the gate then
/// runs at node scope with an empty subject.
// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub fn served_subject(home: &Path, local_node: &str, uri_path: &str) -> Option<String> {
    let relative = sanitize_request_path(uri_path)?;
    let mut components = relative.components();
    let Component::Normal(first) = components.next()? else {
        return None;
    };
    if !first.to_str()?.eq_ignore_ascii_case(local_node) {
        return None;
    }
    let Component::Normal(facet) = components.next()? else {
        return None;
    };
    let facet = facet.to_str()?;
    let registry = registry_at(home).ok()?;
    let entry = match facet {
        "docs" | "m" | "bin" | "install" => return None,
        "f" | "a" => {
            let Component::Normal(name) = components.next()? else {
                return None;
            };
            let name = name.to_str()?;
            if facet == "f" {
                registry.entries().find(|entry| entry.served_name == name)?
            } else {
                registry.entries().find(|entry| entry.adapter.as_deref() == Some(name))?
            }
        }
        alias => registry.entries().find(|entry| entry.short_alias && entry.served_name == alias)?,
    };
    entry.origin.clone()
}

/// Resolve one GET/HEAD path. The production listener owns method selection;
/// this module owns only routing and never reads the process's SPT_HOME.
// [impl->REQ-WEB-URL-NODE-PREFIX]
pub fn handle_path(
    home: &Path,
    docs_root: &Path,
    local_node: &str,
    port: u16,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Response<Full<Bytes>> {
    materialize(resolve_path(home, docs_root, local_node, port, uri_path, query, head_only), head_only, None)
}

/// Decide what one GET/HEAD path names, without moving a byte (module docs).
/// The docs compatibility surface — every first segment that is neither the
/// local node nor a known peer, and every reserved facet at the root — stays
/// byte-for-byte the W0 `serve_path` answer, its 404 included.
// [impl->REQ-WEB-URL-NODE-PREFIX]
// [impl->REQ-WEB-CROSS-NODE-PROXY]
pub fn resolve_path(
    home: &Path,
    docs_root: &Path,
    local_node: &str,
    port: u16,
    uri_path: &str,
    query: Option<&str>,
    head_only: bool,
) -> Resolved {
    let ready = Resolved::Ready;
    if uri_path == "/" {
        return ready(redirect_node(local_node, query));
    }
    let Some(relative) = sanitize_request_path(uri_path) else {
        let raw_first = uri_path.trim_start_matches('/').split('/').next().unwrap_or("");
        if route_segment(raw_first).is_some_and(|first| {
            first.eq_ignore_ascii_case(local_node)
                && !spt_store::hostlabel::is_reserved_web_facet(&first)
        }) {
            return ready(text(StatusCode::BAD_REQUEST, "BAD_PATH: rejected by the docs-root sanitizer\\n".to_owned(), head_only));
        }
        // Preserve the docs-less compatibility response as well as the
        // installed bundle's sanitizer status; no bytes bypass containment.
        return ready(serve_path(docs_root, uri_path, head_only));
    };
    // ADR-0056 Amendment 1: local node, exact docs root-file leaf, known peer,
    // then the unchanged docs compatibility surface (including its docs 404).
    let first = relative.components().next().and_then(|component| match component {
        Component::Normal(first) => first.to_str(),
        _ => None,
    });
    let Some(first) = first else {
        return ready(serve_path(docs_root, uri_path, head_only));
    };
    if spt_store::hostlabel::is_reserved_web_facet(first) {
        return ready(serve_path(docs_root, uri_path, head_only));
    }
    let path = uri_path.trim_start_matches('/');
    let bare_node = !path.contains('/') && route_segment(path).is_some();
    if !first.eq_ignore_ascii_case(local_node) {
        // Rule 2.5 protects root files by URL shape, not hostname policy.
        // A trailing slash or child segment always keeps the node grammar.
        if bare_node && docs_root.join(first).is_file() {
            return ready(serve_path(docs_root, uri_path, head_only));
        }
        if let Some(node_hex) = known_subnet_node(home, first) {
            if bare_node {
                return ready(redirect_node(first, query));
            }
            let remainder = path.split_once('/').map(|(_, rest)| rest).unwrap_or("");
            return peer_arm(first, node_hex, remainder, head_only);
        }
        // An UNKNOWN label is the docs compatibility surface, its 404
        // included — never a 502 (ruled 2026-09-07: 502 names a KNOWN peer).
        return ready(serve_path(docs_root, uri_path, head_only));
    }
    if bare_node {
        return ready(redirect_node(local_node, query));
    }
    let (raw_node, remainder) = path.split_once('/').unwrap_or((path, ""));
    let Some(_) = route_segment(raw_node) else {
        return ready(text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one node segment\\n".to_owned(), head_only));
    };
    if remainder.is_empty() {
        return ready(match registry_at(home) {
            Ok(registry) => index(&registry, local_node, port, query, head_only),
            Err(error) => text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only),
        });
    }
    let (raw_facet, remainder) = remainder.split_once('/').unwrap_or((remainder, ""));
    let Some(facet) = route_segment(raw_facet) else {
        return ready(text(StatusCode::BAD_REQUEST, "BAD_PATH: expected one facet segment\\n".to_owned(), head_only));
    };
    match facet.as_str() {
        "docs" => {
            if remainder.is_empty() && !uri_path.ends_with('/') {
                return ready(redirect_directory(uri_path, query));
            }
            planned(docs_root, remainder, head_only)
        }
        "f" if !remainder.is_empty() => match registry_at(home) {
            Ok(registry) => reference(home, &registry, remainder, uri_path, query, head_only),
            Err(error) => ready(text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only)),
        },
        "a" if !remainder.is_empty() => match registry_at(home) {
            Ok(registry) => adapter(home, &registry, remainder, uri_path, query, head_only),
            Err(error) => ready(text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only)),
        },
        "m" | "bin" | "install" => ready(text(StatusCode::NOT_FOUND, format!("FACET_UNAVAILABLE: {facet}\\n"), head_only)),
        "a" | "f" => ready(text(StatusCode::NOT_FOUND, format!("FACET_NOT_FOUND: {facet}\\n"), head_only)),
        _ => match registry_at(home) {
            Ok(registry) => {
                let entry = registry.entries().find(|entry| entry.short_alias && entry.served_name == facet);
                match entry {
                    Some(entry) => serve_entry(home, entry, remainder, uri_path, query, head_only),
                    None => ready(text(StatusCode::NOT_FOUND, format!("FACET_NOT_FOUND: {facet}\\n"), head_only)),
                }
            }
            Err(error) => ready(text(StatusCode::INTERNAL_SERVER_ERROR, error, head_only)),
        },
    }
}
''')

# Unit tests for W1 appended inside the tests module: anchor = the last test's closing.
rep('''        assert_eq!(get(home.path(), "/local/m/example").status(), StatusCode::NOT_FOUND);
        let response = get(home.path(), "/local/docs/");
        assert_eq!(response.status(), StatusCode::NOT_FOUND);
        assert!(String::from_utf8(body(response)).unwrap().contains("spt update fetch --apply"));
    }
}
''', '''        assert_eq!(get(home.path(), "/local/m/example").status(), StatusCode::NOT_FOUND);
        let response = get(home.path(), "/local/docs/");
        assert_eq!(response.status(), StatusCode::NOT_FOUND);
        assert!(String::from_utf8(body(response)).unwrap().contains("spt update fetch --apply"));
    }

    // [unit->REQ-WEB-CROSS-NODE-PROXY] the one Range grammar: closed, open-
    // ended and suffix forms; a closed end past the last byte clamps; a start
    // past the last byte is unsatisfiable; every other shape is ignored (200).
    #[test]
    fn range_grammar_partial_suffix_clamp_and_unsatisfiable() {
        use RangePlan::*;
        assert_eq!(apply_range(10, None), Full);
        assert_eq!(apply_range(10, Some("bytes=0-3")), Partial { start: 0, end: 3 });
        assert_eq!(apply_range(10, Some("bytes=4-")), Partial { start: 4, end: 9 });
        assert_eq!(apply_range(10, Some("bytes=-3")), Partial { start: 7, end: 9 });
        assert_eq!(apply_range(10, Some("bytes=-30")), Partial { start: 0, end: 9 }, "a long suffix is the whole body, as 206");
        assert_eq!(apply_range(10, Some("bytes=2-99")), Partial { start: 2, end: 9 }, "a closed end clamps");
        assert_eq!(apply_range(10, Some("bytes=10-")), Unsatisfiable);
        assert_eq!(apply_range(10, Some("bytes=-0")), Unsatisfiable);
        assert_eq!(apply_range(0, Some("bytes=0-")), Unsatisfiable, "an empty body satisfies no range");
        for ignored in ["items=0-3", "bytes=3-1", "bytes=0-1,4-5", "bytes=a-b", "bytes=", "0-3"] {
            assert_eq!(apply_range(10, Some(ignored)), Full, "{ignored:?} is ignored, not refused");
        }
        let plan = plan_file(10, "text/plain; charset=utf-8", Some("bytes=0-3"));
        assert_eq!(plan.status, StatusCode::PARTIAL_CONTENT);
        assert_eq!(plan.content_length, 4);
        assert_eq!(plan.content_range.as_deref(), Some("bytes 0-3/10"));
        let plan = plan_file(10, "text/plain; charset=utf-8", Some("bytes=10-"));
        assert_eq!(plan.status, StatusCode::RANGE_NOT_SATISFIABLE);
        assert_eq!(plan.content_length, 0);
        assert_eq!(plan.content_range.as_deref(), Some("bytes */10"));
    }

    // [unit->REQ-WEB-CROSS-NODE-PROXY] a file plan materializes as the
    // range says: 206 with Content-Range and exactly the window's bytes,
    // HEAD keeps the head and drops the body, 416 names the length, and a
    // file that vanished between resolve and read is a 404.
    #[test]
    fn materialize_reads_a_file_plan_through_the_range() {
        let home = tempfile::tempdir().unwrap();
        let source = home.path().join("ten.txt");
        std::fs::write(&source, b"0123456789").unwrap();
        let plan = || Resolved::File { path: source.clone(), content_type: "text/plain; charset=utf-8" };
        let full = materialize(plan(), false, None);
        assert_eq!(full.status(), StatusCode::OK);
        assert_eq!(full.headers()["content-length"], "10");
        assert_eq!(body(full), b"0123456789");
        let part = materialize(plan(), false, Some("bytes=2-5"));
        assert_eq!(part.status(), StatusCode::PARTIAL_CONTENT);
        assert_eq!(part.headers()["content-range"], "bytes 2-5/10");
        assert_eq!(part.headers()["content-length"], "4");
        assert_eq!(body(part), b"2345");
        let head = materialize(plan(), true, Some("bytes=2-5"));
        assert_eq!(head.status(), StatusCode::PARTIAL_CONTENT);
        assert_eq!(head.headers()["content-length"], "4");
        assert!(body(head).is_empty());
        let none = materialize(plan(), false, Some("bytes=10-"));
        assert_eq!(none.status(), StatusCode::RANGE_NOT_SATISFIABLE);
        assert_eq!(none.headers()["content-range"], "bytes */10");
        std::fs::remove_file(&source).unwrap();
        let gone = materialize(plan(), false, None);
        assert_eq!(gone.status(), StatusCode::NOT_FOUND);
        assert!(String::from_utf8(body(gone)).unwrap().contains("ten.txt"));
        let proxy = materialize(Resolved::Proxy { node_label: "peer-x".into(), node_hex: "aa".into() }, false, None);
        assert_eq!(proxy.status(), StatusCode::BAD_GATEWAY);
        assert!(String::from_utf8(body(proxy)).unwrap().starts_with("NODE_UNAVAILABLE: peer-x:"));
    }

    fn seed_peer(home: &Path, label: &str, hex: &str) {
        use spt_store::roster::{RosterEntry, RosterStore};
        use spt_store::subnet::SubnetStore;
        let identity = home.join("identity");
        let mut subnets = SubnetStore::load_from(&identity.join("subnet.json"));
        if subnets.subnets.is_empty() {
            subnets.create_subnet("home", spt_store::access::Mode::Open).unwrap();
            subnets.save_to(&identity.join("subnet.json")).unwrap();
        }
        let mut roster = RosterStore::load_from(&identity.join("roster.json"));
        roster.merge_entry(RosterEntry {
            pubkey_hex: hex.to_owned(),
            subnet: "home".to_owned(),
            label: label.to_owned(),
            machine_id: format!("machine-{hex}"),
            address: None,
            last_seen: "1".to_owned(),
            lease_epoch: 1,
        });
        roster.save_to(&identity.join("roster.json")).unwrap();
    }

    // [unit->REQ-WEB-CROSS-NODE-PROXY] the peer arm's proxy-or-local
    // decision per facet: the index, docs, a named f/ or a/ resource and a
    // short alias open a stream; the bare node still redirects; reserved
    // and unbuilt facets are answered locally without a hop; an UNKNOWN
    // label is the docs compatibility 404, unchanged and never a 502.
    #[test]
    fn peer_paths_proxy_by_facet_and_unknown_labels_stay_docs_404() {
        let home = tempfile::tempdir().unwrap();
        seed_peer(home.path(), "Peer-B", "bb22");
        assert_eq!(known_subnet_node(home.path(), "peer-b").as_deref(), Some("bb22"));
        assert_eq!(known_subnet_node(home.path(), "nobody"), None);
        let resolve = |path: &str| resolve_path(home.path(), &home.path().join("docs"), "LOCAL", 5474, path, None, false);
        for path in ["/peer-b/", "/Peer-B/docs/", "/peer-b/docs/cli/reference.md", "/peer-b/f/report.md", "/peer-b/a/example/out.txt", "/peer-b/short/", "/peer-b/f/dir/child.txt"] {
            match resolve(path) {
                Resolved::Proxy { node_label, node_hex } => {
                    assert_eq!(node_hex, "bb22", "{path}");
                    assert!(node_label.eq_ignore_ascii_case("peer-b"), "{path}: {node_label}");
                }
                _ => panic!("{path} must proxy to the owner"),
            }
        }
        let bare = materialize(resolve("/peer-b"), false, None);
        assert_eq!(bare.status(), StatusCode::FOUND);
        assert_eq!(bare.headers()["location"], "/peer-b/");
        for (path, marker) in [("/peer-b/m/x", "FACET_UNAVAILABLE: m"), ("/peer-b/bin/spt", "FACET_UNAVAILABLE: bin"), ("/peer-b/install", "FACET_UNAVAILABLE: install"), ("/peer-b/f/", "FACET_NOT_FOUND: f"), ("/peer-b/f", "FACET_NOT_FOUND: f"), ("/peer-b/a/", "FACET_NOT_FOUND: a")] {
            let response = match resolve(path) {
                Resolved::Ready(response) => response,
                _ => panic!("{path} is answered locally, never proxied"),
            };
            assert_eq!(response.status(), StatusCode::NOT_FOUND, "{path}");
            assert!(String::from_utf8(body(response)).unwrap().starts_with(marker), "{path}");
        }
        let unknown = materialize(resolve("/nobody/f/report.md"), false, None);
        let docs = serve_path(&home.path().join("docs"), "/nobody/f/report.md", false);
        assert_eq!(unknown.status(), StatusCode::NOT_FOUND);
        assert_eq!(body(unknown), body(docs), "the docs compatibility 404, byte for byte");
    }

    // [unit->REQ-WEB-CROSS-NODE-PROXY] the owner's gate subject is the
    // served entry's registering endpoint, resolved through the same registry
    // the local facet reads; paths that name no entry gate at node scope.
    #[test]
    fn served_subject_is_the_entrys_origin_or_none() {
        let home = tempfile::tempdir().unwrap();
        let source = home.path().join("report.md");
        std::fs::write(&source, b"x").unwrap();
        let registry_path = spt_store::perch::serving_registry_file_in(home.path());
        let mut registry = ServingRegistry::load_at(&registry_path).unwrap();
        registry.add_reference(&source, Some("report.md"), Some("ling"), 1).unwrap();
        registry.save_at(&registry_path).unwrap();
        let entry = add_adapter(home.path(), "example", Some("short"));
        assert_eq!(served_subject(home.path(), "LOCAL", "/local/f/report.md").as_deref(), Some("ling"));
        assert_eq!(served_subject(home.path(), "LOCAL", "/LOCAL/f/report.md").as_deref(), Some("ling"));
        assert_eq!(served_subject(home.path(), "LOCAL", "/local/a/example/out.txt"), entry.origin);
        assert_eq!(served_subject(home.path(), "LOCAL", "/local/short/out.txt"), entry.origin);
        for path in ["/local/", "/local/docs/index.html", "/local/f/missing.md", "/other/f/report.md", "/local/m/x"] {
            assert_eq!(served_subject(home.path(), "LOCAL", path), None, "{path}");
        }
    }
}
''')
p.write_text(s, encoding="utf-8")
print("webserve edited")
