//! Validation-gated project build and deterministic runtime asset packaging.

use std::collections::BTreeSet;
use std::fs::{self, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, Write};
use std::path::{Component, Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};
use walkdir::WalkDir;

const PACKAGE_SCHEMA_VERSION: u32 = 1;

#[derive(Debug, Clone, Deserialize)]
struct BuildProfile {
    version: u32,
    name: String,
    #[serde(default)]
    cargo_profile: CargoProfile,
    #[serde(default)]
    target: Option<String>,
    #[serde(default)]
    features: Vec<String>,
    rendering_tier: RenderingTier,
    #[serde(default)]
    default_scene: Option<String>,
    output_dir: String,
}

#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
enum CargoProfile {
    #[default]
    Development,
    Qa,
    Release,
}

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
enum RenderingTier {
    Development,
    Representative,
    Shipping,
}

impl<'de> Deserialize<'de> for RenderingTier {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        match value.as_str() {
            "development" => Ok(Self::Development),
            "representative" => Ok(Self::Representative),
            "shipping" => Ok(Self::Shipping),
            _ => Err(serde::de::Error::custom(format!(
                "unknown rendering tier `{value}`"
            ))),
        }
    }
}

impl CargoProfile {
    fn cargo_name(self) -> &'static str {
        match self {
            Self::Development => "package-dev",
            Self::Qa => "package-qa",
            Self::Release => "release",
        }
    }
}

#[derive(Debug, Serialize)]
struct PackageMetadata {
    schema_version: u32,
    profile: String,
    cargo_profile: CargoProfile,
    target: String,
    features: Vec<String>,
    rendering_tier: RenderingTier,
    default_scene: Option<String>,
    git_commit: String,
    git_dirty: bool,
    project_git_commit: Option<String>,
    project_git_dirty: Option<bool>,
    rustc: String,
    cargo: String,
    generated_unix_secs: u64,
    executable: String,
    debug_symbols: Option<String>,
    content: Vec<ContentEntry>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct ContentEntry {
    path: String,
    byte_len: u64,
    blake3: String,
}

#[derive(Debug, Default, PartialEq, Eq)]
struct CopyStats {
    written: usize,
    reused: usize,
    pruned: usize,
}

#[derive(Debug)]
struct Options {
    project_root: PathBuf,
    profile_name: String,
    skip_build: bool,
    artifact: Option<PathBuf>,
}

#[derive(Debug)]
struct BuiltArtifacts {
    executable: PathBuf,
    companions: Vec<PathBuf>,
}

fn main() {
    match run(parse_options()) {
        Ok(output) => println!("package-project: ready at {}", output.display()),
        Err(error) => {
            eprintln!("package-project: {error}");
            std::process::exit(1);
        }
    }
}

fn parse_options() -> Options {
    let mut options = Options {
        project_root: PathBuf::from("."),
        profile_name: "development".into(),
        skip_build: false,
        artifact: None,
    };
    let mut args = std::env::args().skip(1);
    while let Some(argument) = args.next() {
        match argument.as_str() {
            "--project" => {
                options.project_root = PathBuf::from(
                    args.next()
                        .unwrap_or_else(|| argument_error("--project requires a path")),
                );
            }
            "--profile" => {
                options.profile_name = args
                    .next()
                    .unwrap_or_else(|| argument_error("--profile requires a name"));
            }
            "--skip-build" => options.skip_build = true,
            "--artifact" => {
                options.artifact =
                    Some(PathBuf::from(args.next().unwrap_or_else(|| {
                        argument_error("--artifact requires a path")
                    })));
            }
            _ => argument_error(&format!("unknown argument `{argument}`")),
        }
    }
    options
}

fn argument_error(message: &str) -> ! {
    eprintln!("package-project: {message}");
    std::process::exit(2);
}

fn run(options: Options) -> Result<PathBuf, String> {
    let engine_root = engine_workspace_root()?;
    let project_root = options
        .project_root
        .canonicalize()
        .map_err(|error| format!("could not resolve project root: {error}"))?;
    let profile = load_profile(&project_root, &options.profile_name)?;
    validate_profile(&project_root, &profile)?;
    let output = resolve_output(&project_root, &profile.output_dir)?;

    validate_release_ready(&project_root, "before compilation")?;

    let built_artifacts = if options.skip_build {
        let artifact = options.artifact.ok_or_else(|| {
            "--skip-build requires --artifact <path> from a matching successful Cargo build"
                .to_string()
        })?;
        let artifact = artifact
            .canonicalize()
            .map_err(|error| format!("could not resolve supplied artifact: {error}"))?;
        let artifact_name = artifact.file_name().and_then(|name| name.to_str());
        if !matches!(artifact_name, Some("game" | "game.exe")) {
            return Err(
                "supplied artifact must be a matching Cargo `game` or `game.exe` binary".into(),
            );
        }
        if let Some(target) = &profile.target {
            let expected_name = if target.contains("windows") {
                "game.exe"
            } else {
                "game"
            };
            if artifact_name != Some(expected_name) {
                return Err(format!(
                    "supplied artifact does not match profile target `{target}`"
                ));
            }
        }
        let pdb = artifact.with_extension("pdb");
        BuiltArtifacts {
            executable: artifact,
            companions: pdb.is_file().then_some(pdb).into_iter().collect(),
        }
    } else {
        build_game(&engine_root, &profile)?
    };
    let executable_source = &built_artifacts.executable;
    if !executable_source.is_file() {
        return Err(format!(
            "built executable is missing at {}",
            executable_source.display()
        ));
    }

    // The compile can take minutes while the editor remains live. Capture and validate the exact
    // source state used for this package after the build, then reject any concurrent mutation.
    let expected_sources = fingerprint_runtime_assets(&project_root)?;
    validate_release_ready(&project_root, "after compilation")?;

    let effective_target = effective_build_target(&engine_root, &profile, executable_source);
    validate_supported_target(&effective_target)?;
    let debug_symbols = select_debug_symbols(&profile, &effective_target, &built_artifacts)?;
    let _package_lock = PackageLock::acquire(&output)?;
    recover_interrupted_package(&output)?;
    let executable_name = packaged_executable_name(&effective_target, executable_source);
    let mut pending = PendingPackage::new(&output)?;
    let mut stats = CopyStats::default();
    pending.seed_from(&output)?;
    prune_package_root(
        pending.path(),
        executable_name,
        debug_symbols.as_ref().map(|(_, name)| *name),
        &mut stats,
    )?;
    copy_if_changed(
        executable_source,
        &pending.path().join(executable_name),
        &mut stats,
    )?;
    if let Some((source, name)) = &debug_symbols {
        copy_if_changed(source, &pending.path().join(*name), &mut stats)?;
    }

    let assets_output = pending.path().join("assets");
    let (mut content, copied_sources) = package_assets(
        &project_root,
        &assets_output,
        profile.default_scene.as_deref(),
        profile.rendering_tier,
        &mut stats,
    )?;
    let current_sources = fingerprint_runtime_assets(&project_root)?;
    if copied_sources != expected_sources || current_sources != expected_sources {
        return Err(
            "project assets changed while the package was being assembled; no output was replaced, retry the build"
                .into(),
        );
    }
    content.push(content_entry(
        &pending.path().join(executable_name),
        executable_name,
    )?);
    if let Some((_, name)) = &debug_symbols {
        content.push(content_entry(&pending.path().join(*name), name)?);
    }
    content.sort_by(|left, right| left.path.cmp(&right.path));
    let metadata = PackageMetadata {
        schema_version: PACKAGE_SCHEMA_VERSION,
        profile: profile.name.clone(),
        cargo_profile: profile.cargo_profile,
        target: effective_target,
        features: profile.features.clone(),
        rendering_tier: profile.rendering_tier,
        default_scene: profile.default_scene.clone(),
        git_commit: command_output(&engine_root, "git", &["rev-parse", "HEAD"]),
        git_dirty: !command_output(&engine_root, "git", &["status", "--porcelain"]).is_empty(),
        project_git_commit: command_output_optional(&project_root, "git", &["rev-parse", "HEAD"]),
        project_git_dirty: command_output_optional(
            &project_root,
            "git",
            &["status", "--porcelain"],
        )
        .map(|status| !status.is_empty()),
        rustc: command_output(&engine_root, "rustc", &["--version"]),
        cargo: command_output(&engine_root, "cargo", &["--version"]),
        generated_unix_secs: SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs(),
        executable: executable_name.into(),
        debug_symbols: debug_symbols.map(|(_, name)| name.to_string()),
        content,
    };
    let metadata_text = serde_json::to_string_pretty(&metadata)
        .map_err(|error| format!("could not serialize build metadata: {error}"))?;
    write_file_atomically(
        &pending.path().join("build-metadata.json"),
        metadata_text.as_bytes(),
    )
    .map_err(|error| format!("could not write build metadata: {error}"))?;
    verify_staged_content(pending.path(), &metadata.content)?;
    pending.publish(&output)?;
    println!(
        "package-project: {} file(s) written, {} unchanged file(s) reused, {} stale file(s) pruned",
        stats.written, stats.reused, stats.pruned
    );
    Ok(output)
}

fn validate_release_ready(project_root: &Path, phase: &str) -> Result<(), String> {
    let validation = scene::validate_project(project_root);
    if validation.is_release_ready() {
        return Ok(());
    }
    let summary = validation
        .findings
        .iter()
        .filter(|finding| finding.severity == scene::ValidationSeverity::Error)
        .take(8)
        .map(|finding| {
            format!(
                "[{}] {}: {}",
                finding.code, finding.source_path, finding.message
            )
        })
        .collect::<Vec<_>>()
        .join("\n");
    Err(format!(
        "content validation failed {phase} with {} blocking error(s)\n{summary}",
        validation.blocking_error_count()
    ))
}

fn load_profile(project_root: &Path, profile_name: &str) -> Result<BuildProfile, String> {
    if profile_name.is_empty()
        || !profile_name
            .chars()
            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_'))
    {
        return Err("profile name must contain only letters, digits, '-' or '_'".into());
    }
    let path = project_root
        .join("assets/build_profiles")
        .join(format!("{profile_name}.ron"));
    let text = fs::read_to_string(&path)
        .map_err(|error| format!("could not read build profile {}: {error}", path.display()))?;
    ron::from_str(&text)
        .map_err(|error| format!("could not parse build profile {}: {error}", path.display()))
}

fn validate_profile(project_root: &Path, profile: &BuildProfile) -> Result<(), String> {
    if profile.version != 1 {
        return Err(format!(
            "unsupported build profile version {}",
            profile.version
        ));
    }
    if profile.name.trim().is_empty() {
        return Err("build profile name must not be empty".into());
    }
    if let Some(target) = &profile.target {
        if target.trim().is_empty()
            || !target.chars().all(|character| {
                character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')
            })
        {
            return Err(format!("invalid Rust target triple `{target}`"));
        }
        validate_supported_target(target)?;
    }
    if let Some(default_scene) = &profile.default_scene {
        let path = resolve_project_asset(project_root, default_scene)
            .ok_or_else(|| format!("default scene `{default_scene}` escapes the project"))?;
        let relative = path
            .strip_prefix(project_root.join("assets"))
            .map_err(|_| format!("default scene `{default_scene}` is outside runtime assets"))?;
        if !relative.starts_with("levels") || relative == Path::new("levels") {
            return Err(format!(
                "default scene `{default_scene}` must be under assets/levels/ so full project dependency validation covers it"
            ));
        }
        let metadata = fs::symlink_metadata(&path).ok();
        if !default_scene.ends_with(".scn.ron")
            || !metadata.as_ref().is_some_and(fs::Metadata::is_file)
        {
            return Err(format!(
                "default scene `{default_scene}` is missing or is not a .scn.ron scene"
            ));
        }
        if metadata.is_some_and(|metadata| metadata.file_type().is_symlink())
            || !include_runtime_asset(relative)
        {
            return Err(format!(
                "default scene `{default_scene}` is not an included regular runtime asset"
            ));
        }
        scene::validate_level_file(&path)
            .map_err(|error| format!("default scene `{default_scene}` is invalid: {error}"))?;
        scene::validate_composition_graph(&path, project_root).map_err(|error| {
            format!("default scene `{default_scene}` has an invalid composition graph: {error}")
        })?;
    }
    let mut features = BTreeSet::new();
    for feature in &profile.features {
        if feature.trim().is_empty()
            || !feature.chars().all(|character| {
                character.is_ascii_alphanumeric() || matches!(character, '-' | '_')
            })
        {
            return Err(format!("invalid Cargo feature `{feature}`"));
        }
        if !features.insert(feature) {
            return Err(format!("duplicate Cargo feature `{feature}`"));
        }
        if matches!(feature.as_str(), "dev" | "hot-reload") {
            return Err(format!(
                "Cargo feature `{feature}` uses runtime dynamic libraries and cannot be packaged"
            ));
        }
    }
    Ok(())
}

fn validate_supported_target(target: &str) -> Result<(), String> {
    let segments = target.split('-').collect::<Vec<_>>();
    let is_windows = segments.contains(&"windows");
    let is_desktop_linux = segments.contains(&"linux") && !segments.contains(&"android");
    if is_windows || is_desktop_linux {
        Ok(())
    } else {
        Err(format!(
            "target `{target}` is not supported by the native package layout; use a desktop Linux or Windows target"
        ))
    }
}

fn build_game(project_root: &Path, profile: &BuildProfile) -> Result<BuiltArtifacts, String> {
    let mut command = Command::new("cargo");
    command.current_dir(project_root).args([
        "build",
        "-p",
        "game",
        "--profile",
        profile.cargo_profile.cargo_name(),
        "--message-format=json-render-diagnostics",
    ]);
    if let Some(target) = &profile.target {
        command.args(["--target", target]);
    }
    if !profile.features.is_empty() {
        command.args(["--features", &profile.features.join(",")]);
    }
    command.stdout(Stdio::piped());
    let mut child = command
        .spawn()
        .map_err(|error| format!("could not start Cargo build: {error}"))?;
    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| "Cargo build stdout was not captured".to_string())?;
    let mut executable = None;
    let mut companions = Vec::new();
    let mut read_error = None;
    for line in BufReader::new(stdout).lines() {
        let line = match line {
            Ok(line) => line,
            Err(error) => {
                read_error = Some(format!("could not read Cargo output: {error}"));
                break;
            }
        };
        let Ok(message) = serde_json::from_str::<serde_json::Value>(&line) else {
            println!("{line}");
            continue;
        };
        if let Some(rendered) = message
            .get("message")
            .and_then(|message| message.get("rendered"))
            .and_then(serde_json::Value::as_str)
        {
            eprint!("{rendered}");
        }
        let is_game_binary = message.get("reason").and_then(serde_json::Value::as_str)
            == Some("compiler-artifact")
            && message
                .get("target")
                .and_then(|target| target.get("name"))
                .and_then(serde_json::Value::as_str)
                == Some("game")
            && message
                .get("target")
                .and_then(|target| target.get("kind"))
                .and_then(serde_json::Value::as_array)
                .is_some_and(|kinds| kinds.iter().any(|kind| kind.as_str() == Some("bin")));
        if is_game_binary {
            executable = message
                .get("executable")
                .and_then(serde_json::Value::as_str)
                .map(PathBuf::from);
            companions = message
                .get("filenames")
                .and_then(serde_json::Value::as_array)
                .into_iter()
                .flatten()
                .filter_map(serde_json::Value::as_str)
                .map(PathBuf::from)
                .collect();
        }
    }
    if let Some(error) = read_error {
        let _ = child.kill();
        let _ = child.wait();
        return Err(error);
    }
    let status = child
        .wait()
        .map_err(|error| format!("could not wait for Cargo build: {error}"))?;
    if !status.success() {
        return Err(format!("Cargo build failed with status {status}"));
    }
    let executable =
        executable.ok_or("Cargo did not report the built `game` executable artifact")?;
    companions.retain(|path| path != &executable);
    companions.sort();
    companions.dedup();
    Ok(BuiltArtifacts {
        executable,
        companions,
    })
}

fn select_debug_symbols(
    profile: &BuildProfile,
    effective_target: &str,
    artifacts: &BuiltArtifacts,
) -> Result<Option<(PathBuf, &'static str)>, String> {
    if profile.cargo_profile != CargoProfile::Qa {
        return Ok(None);
    }
    let pdb = artifacts.companions.iter().find(|path| {
        path.extension()
            .and_then(|extension| extension.to_str())
            .is_some_and(|extension| extension.eq_ignore_ascii_case("pdb"))
    });
    if effective_target.contains("windows") && effective_target.contains("msvc") && pdb.is_none() {
        return Err("Cargo did not report the PDB required by the Windows MSVC QA profile".into());
    }
    Ok(pdb.map(|path| (path.clone(), "blacksite.pdb")))
}

fn packaged_executable_name(effective_target: &str, artifact: &Path) -> &'static str {
    if effective_target.contains("windows")
        || artifact
            .extension()
            .and_then(|extension| extension.to_str())
            == Some("exe")
    {
        "blacksite.exe"
    } else {
        "blacksite"
    }
}

fn effective_build_target(engine_root: &Path, profile: &BuildProfile, artifact: &Path) -> String {
    if let Some(target) = &profile.target {
        return target.clone();
    }
    if let Some(target_directory) = cargo_target_directory(engine_root) {
        let target_directory = target_directory.canonicalize().unwrap_or(target_directory);
        let artifact = artifact
            .canonicalize()
            .unwrap_or_else(|_| artifact.to_path_buf());
        if let Ok(relative) = artifact.strip_prefix(target_directory) {
            let components = relative.components().collect::<Vec<_>>();
            if let Some(profile_index) = components
                .iter()
                .position(|component| component.as_os_str() == profile.cargo_profile.cargo_name())
            {
                if profile_index > 0 {
                    return components[profile_index - 1]
                        .as_os_str()
                        .to_string_lossy()
                        .into_owned();
                }
            }
        }
    }
    host_target(engine_root)
}

fn cargo_target_directory(engine_root: &Path) -> Option<PathBuf> {
    let output = Command::new("cargo")
        .current_dir(engine_root)
        .args(["metadata", "--format-version=1", "--no-deps", "--offline"])
        .output()
        .ok()?
        .stdout;
    let metadata: serde_json::Value = serde_json::from_slice(&output).ok()?;
    metadata
        .get("target_directory")
        .and_then(serde_json::Value::as_str)
        .map(PathBuf::from)
}

fn engine_workspace_root() -> Result<PathBuf, String> {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .ok_or_else(|| "xtask must live under the engine workspace".to_string())?
        .canonicalize()
        .map_err(|error| format!("could not resolve engine workspace: {error}"))
}

fn resolve_output(project_root: &Path, output: &str) -> Result<PathBuf, String> {
    let path = Path::new(output);
    if path.as_os_str().is_empty()
        || path == Path::new(".")
        || path.is_absolute()
        || path.components().any(|component| {
            matches!(
                component,
                Component::CurDir
                    | Component::ParentDir
                    | Component::RootDir
                    | Component::Prefix(_)
            )
        })
    {
        return Err("build output must be a project-relative path".into());
    }
    if !path.starts_with("dist") || path == Path::new("dist") {
        return Err("build output must be inside a dedicated dist/<name> directory".into());
    }
    if ["assets", "target", ".git", ".cargo"]
        .iter()
        .any(|protected| path.starts_with(protected))
    {
        return Err("build output cannot be inside assets, target, .git, or .cargo".into());
    }
    let lexical_output = project_root.join(path);
    let mut existing = lexical_output.as_path();
    while fs::symlink_metadata(existing).is_err() {
        existing = existing
            .parent()
            .ok_or_else(|| "build output has no project ancestor".to_string())?;
    }
    let root = project_root
        .canonicalize()
        .map_err(|error| format!("could not resolve project root: {error}"))?;
    let dist = root.join("dist");
    if fs::symlink_metadata(&dist)
        .map(|metadata| metadata.file_type().is_symlink())
        .unwrap_or(false)
    {
        return Err("project dist directory cannot be a symlink".into());
    }
    let resolved_dist = dist.canonicalize().unwrap_or(dist);
    let resolved_ancestor = existing
        .canonicalize()
        .map_err(|error| format!("could not resolve build output ancestor: {error}"))?;
    if !resolved_ancestor.starts_with(&root) {
        return Err("build output escapes the project through a symlink".into());
    }
    let suffix = lexical_output
        .strip_prefix(existing)
        .map_err(|error| format!("could not resolve build output suffix: {error}"))?;
    let resolved_output = resolved_ancestor.join(suffix);
    if resolved_output == resolved_dist || !resolved_output.starts_with(&resolved_dist) {
        return Err("build output must resolve inside the project dist directory".into());
    }
    for protected in ["assets", "target", ".git", ".cargo"] {
        let protected = root.join(protected);
        let protected = protected.canonicalize().unwrap_or(protected);
        if resolved_output.starts_with(protected) {
            return Err("build output cannot be inside assets, target, .git, or .cargo".into());
        }
    }
    Ok(resolved_output)
}

const PACKAGE_LOCK_MAGIC: &str = "blacksite-package-lock";

#[derive(Debug, Serialize, Deserialize)]
struct PackageLockRecord {
    magic: String,
    schema_version: u32,
    output: String,
    pid: u32,
}

struct PackageLock {
    file: fs::File,
}

impl PackageLock {
    fn acquire(output: &Path) -> Result<Self, String> {
        let parent = output
            .parent()
            .ok_or_else(|| "package output has no parent directory".to_string())?;
        fs::create_dir_all(parent)
            .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
        let stem = output
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("package");
        let path = parent.join(format!(".blacksite-{stem}.lock"));
        if fs::symlink_metadata(&path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
            return Err(format!(
                "refusing to open symlinked package lock {}",
                path.display()
            ));
        }
        let mut options = OpenOptions::new();
        options.read(true).write(true).create(true);
        #[cfg(unix)]
        {
            use std::os::unix::fs::OpenOptionsExt;
            options.custom_flags(libc::O_NOFOLLOW);
        }
        #[cfg(windows)]
        {
            use std::os::windows::fs::OpenOptionsExt;
            const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
            options
                .share_mode(0)
                .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
        }
        let mut file = options.open(&path).map_err(|error| {
            format!(
                "could not acquire package lock {} (another build may be active): {error}",
                path.display()
            )
        })?;
        if !file
            .metadata()
            .map_err(|error| format!("could not inspect package lock {}: {error}", path.display()))?
            .is_file()
        {
            return Err(format!(
                "package lock is not a regular file: {}",
                path.display()
            ));
        }
        lock_file_exclusive(&file).map_err(|error| {
            format!(
                "could not acquire package lock {} (another build may be active): {error}",
                path.display()
            )
        })?;

        let mut existing = String::new();
        file.read_to_string(&mut existing)
            .map_err(|error| format!("could not read package lock {}: {error}", path.display()))?;
        let output_identity = output.to_string_lossy().into_owned();
        if !existing.trim().is_empty() {
            let record: PackageLockRecord = serde_json::from_str(&existing).map_err(|error| {
                format!(
                    "refusing to overwrite unrecognized package lock {}: {error}",
                    path.display()
                )
            })?;
            if record.magic != PACKAGE_LOCK_MAGIC
                || record.schema_version != PACKAGE_SCHEMA_VERSION
                || record.output != output_identity
            {
                return Err(format!(
                    "refusing to overwrite unrecognized package lock {}",
                    path.display()
                ));
            }
        }
        let record = PackageLockRecord {
            magic: PACKAGE_LOCK_MAGIC.into(),
            schema_version: PACKAGE_SCHEMA_VERSION,
            output: output_identity,
            pid: std::process::id(),
        };
        let mut encoded = serde_json::to_vec(&record)
            .map_err(|error| format!("could not serialize package lock: {error}"))?;
        encoded.push(b'\n');
        let write_result = (|| -> std::io::Result<()> {
            file.seek(std::io::SeekFrom::Start(0))?;
            file.write_all(&encoded)?;
            file.set_len(encoded.len() as u64)?;
            file.sync_all()
        })();
        if let Err(error) = write_result {
            let _ = file.seek(std::io::SeekFrom::Start(0));
            let _ = file.write_all(existing.as_bytes());
            let _ = file.set_len(existing.len() as u64);
            let _ = file.sync_all();
            return Err(format!(
                "could not persist package lock {}: {error}",
                path.display()
            ));
        }
        Ok(Self { file })
    }
}

impl Drop for PackageLock {
    fn drop(&mut self) {
        unlock_file(&self.file);
    }
}

#[cfg(unix)]
fn lock_file_exclusive(file: &fs::File) -> std::io::Result<()> {
    use std::os::fd::AsRawFd;

    let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
    (result == 0)
        .then_some(())
        .ok_or_else(std::io::Error::last_os_error)
}

#[cfg(windows)]
fn lock_file_exclusive(_file: &fs::File) -> std::io::Result<()> {
    // The zero-share-mode handle acquired above is the exclusive lock.
    Ok(())
}

#[cfg(not(any(unix, windows)))]
fn lock_file_exclusive(_file: &fs::File) -> std::io::Result<()> {
    Err(std::io::Error::new(
        std::io::ErrorKind::Unsupported,
        "package locking is unsupported on this host",
    ))
}

#[cfg(unix)]
fn unlock_file(file: &fs::File) {
    use std::os::fd::AsRawFd;

    let _ = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
}

#[cfg(not(unix))]
fn unlock_file(_file: &fs::File) {}

const PACKAGE_WORK_MAGIC: &str = "blacksite-package-staging";
const PACKAGE_WORK_MARKER: &str = ".blacksite-package-work.json";

#[derive(Debug, Serialize, Deserialize)]
struct PackageWorkMarker {
    magic: String,
    schema_version: u32,
    output: String,
}

fn write_work_marker(staging: &Path, output: &Path) -> Result<(), String> {
    let marker = PackageWorkMarker {
        magic: PACKAGE_WORK_MAGIC.into(),
        schema_version: PACKAGE_SCHEMA_VERSION,
        output: output.to_string_lossy().into_owned(),
    };
    let bytes = serde_json::to_vec(&marker)
        .map_err(|error| format!("could not serialize staging ownership: {error}"))?;
    write_file_atomically(&staging.join(PACKAGE_WORK_MARKER), &bytes)
}

fn verify_work_marker(staging: &Path, output: &Path) -> Result<(), String> {
    let metadata = fs::symlink_metadata(staging).map_err(|error| {
        format!(
            "could not inspect interrupted staging path {}: {error}",
            staging.display()
        )
    })?;
    if !metadata.is_dir() || metadata.file_type().is_symlink() {
        return Err(format!(
            "refusing to remove unowned staging path {}",
            staging.display()
        ));
    }
    let marker_path = staging.join(PACKAGE_WORK_MARKER);
    let marker_metadata = fs::symlink_metadata(&marker_path).map_err(|_| {
        format!(
            "refusing to remove unowned staging directory {}; ownership marker is missing",
            staging.display()
        )
    })?;
    if !marker_metadata.is_file() || marker_metadata.file_type().is_symlink() {
        return Err(format!(
            "refusing to remove staging directory with invalid ownership marker {}",
            marker_path.display()
        ));
    }
    let marker: PackageWorkMarker = serde_json::from_slice(
        &fs::read(&marker_path)
            .map_err(|error| format!("could not read {}: {error}", marker_path.display()))?,
    )
    .map_err(|error| format!("invalid staging ownership marker: {error}"))?;
    if marker.magic != PACKAGE_WORK_MAGIC
        || marker.schema_version != PACKAGE_SCHEMA_VERSION
        || marker.output != output.to_string_lossy().as_ref()
    {
        return Err(format!(
            "refusing to remove staging directory with unrecognized ownership marker {}",
            marker_path.display()
        ));
    }
    Ok(())
}

fn recover_interrupted_package(output: &Path) -> Result<(), String> {
    let parent = output
        .parent()
        .ok_or_else(|| "package output has no parent directory".to_string())?;
    let stem = output
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("package");
    let prefix = format!(".blacksite-{stem}-");
    let mut staging = Vec::new();
    let mut backups = Vec::new();
    for entry in fs::read_dir(parent)
        .map_err(|error| format!("could not inspect {}: {error}", parent.display()))?
    {
        let entry =
            entry.map_err(|error| format!("could not inspect package work directory: {error}"))?;
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if !name.starts_with(&prefix) {
            continue;
        }
        match entry
            .path()
            .extension()
            .and_then(|extension| extension.to_str())
        {
            Some("staging") => staging.push(entry.path()),
            Some("backup") => backups.push(entry.path()),
            _ => {}
        }
    }
    for path in staging {
        verify_work_marker(&path, output)?;
        remove_path(&path)?;
        eprintln!(
            "package-project: removed interrupted staging directory {}",
            path.display()
        );
    }

    let output_is_empty = output.is_dir()
        && fs::read_dir(output)
            .map_err(|error| format!("could not inspect {}: {error}", output.display()))?
            .next()
            .is_none();
    if output_is_empty {
        fs::remove_dir(output)
            .map_err(|error| format!("could not remove empty {}: {error}", output.display()))?;
    }
    backups.sort_by_key(|path| {
        std::cmp::Reverse(
            fs::metadata(path)
                .and_then(|metadata| metadata.modified())
                .unwrap_or(UNIX_EPOCH),
        )
    });
    if !output.exists() {
        if let Some(backup) = backups.first() {
            verify_strict_owned_package(backup)?;
            fs::rename(backup, output).map_err(|error| {
                format!(
                    "could not restore interrupted package backup {} to {}: {error}",
                    backup.display(),
                    output.display()
                )
            })?;
            eprintln!(
                "package-project: restored previous package from {}",
                backup.display()
            );
        }
    } else {
        verify_owned_output(output)?;
    }
    let published_marker = output.join(PACKAGE_WORK_MARKER);
    if published_marker.exists() {
        verify_work_marker(output, output)?;
        fs::remove_file(&published_marker).map_err(|error| {
            format!(
                "could not remove recovered staging marker {}: {error}",
                published_marker.display()
            )
        })?;
    }
    for backup in backups {
        if backup.exists() {
            verify_strict_owned_package(&backup)?;
            remove_path(&backup)?;
        }
    }
    Ok(())
}

struct PendingPackage {
    path: PathBuf,
    published: bool,
}

impl PendingPackage {
    fn new(output: &Path) -> Result<Self, String> {
        if output.exists() && !output.is_dir() {
            return Err(format!(
                "package output exists but is not a directory: {}",
                output.display()
            ));
        }
        verify_owned_output(output)?;
        let parent = output
            .parent()
            .ok_or_else(|| "package output has no parent directory".to_string())?;
        fs::create_dir_all(parent)
            .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
        let stem = output
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("package");
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        for attempt in 0..100_u32 {
            let path = parent.join(format!(
                ".blacksite-{stem}-{}-{nonce}-{attempt}.staging",
                std::process::id()
            ));
            match fs::create_dir(&path) {
                Ok(()) => {
                    if let Err(error) = write_work_marker(&path, output) {
                        let _ = fs::remove_dir_all(&path);
                        return Err(error);
                    }
                    return Ok(Self {
                        path,
                        published: false,
                    });
                }
                Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
                Err(error) => {
                    return Err(format!(
                        "could not create package staging directory {}: {error}",
                        path.display()
                    ));
                }
            }
        }
        Err("could not reserve a unique package staging directory".into())
    }

    fn path(&self) -> &Path {
        &self.path
    }

    fn seed_from(&self, output: &Path) -> Result<(), String> {
        if output.is_dir() {
            seed_directory(output, &self.path)?;
        }
        Ok(())
    }

    fn publish(&mut self, output: &Path) -> Result<(), String> {
        let backup = self.path.with_extension("backup");
        if fs::symlink_metadata(&backup).is_ok() {
            return Err(format!(
                "reserved package backup path already exists: {}",
                backup.display()
            ));
        }
        let had_previous = output.exists();
        if had_previous {
            fs::rename(output, &backup).map_err(|error| {
                format!(
                    "could not move previous package {} aside: {error}",
                    output.display()
                )
            })?;
        }
        if let Err(error) = fs::rename(&self.path, output) {
            if had_previous {
                if let Err(restore_error) = fs::rename(&backup, output) {
                    return Err(format!(
                        "could not promote staged package to {}; restoring the previous package also failed: {restore_error}. The previous package remains at {} (promotion error: {error})",
                        output.display(),
                        backup.display()
                    ));
                }
            }
            return Err(format!(
                "could not promote staged package to {}: {error}",
                output.display()
            ));
        }
        self.published = true;
        let published_marker = output.join(PACKAGE_WORK_MARKER);
        if let Err(error) = fs::remove_file(&published_marker) {
            eprintln!(
                "package-project: warning: could not remove staging marker {}: {error}",
                published_marker.display()
            );
        }
        if had_previous {
            if let Err(error) = remove_path(&backup) {
                eprintln!(
                    "package-project: warning: could not remove old package backup {}: {error}",
                    backup.display()
                );
            }
        }
        Ok(())
    }
}

fn verify_owned_output(output: &Path) -> Result<(), String> {
    verify_owned_output_inner(output, true)
}

fn verify_strict_owned_package(output: &Path) -> Result<(), String> {
    verify_owned_output_inner(output, false)
}

fn verify_owned_output_inner(output: &Path, allow_missing_or_empty: bool) -> Result<(), String> {
    let output_metadata = match fs::symlink_metadata(output) {
        Ok(metadata) => metadata,
        Err(error) if allow_missing_or_empty && error.kind() == std::io::ErrorKind::NotFound => {
            return Ok(());
        }
        Err(error) => {
            return Err(format!(
                "could not inspect package output {}: {error}",
                output.display()
            ));
        }
    };
    if !output_metadata.is_dir() || output_metadata.file_type().is_symlink() {
        return Err(format!(
            "package output is not a regular directory: {}",
            output.display()
        ));
    }
    let mut entries = fs::read_dir(output)
        .map_err(|error| format!("could not inspect {}: {error}", output.display()))?;
    if entries.next().is_none() {
        return if allow_missing_or_empty {
            Ok(())
        } else {
            Err(format!(
                "package backup is empty and has no ownership metadata: {}",
                output.display()
            ))
        };
    }
    let metadata_path = output.join("build-metadata.json");
    let metadata_file = fs::symlink_metadata(&metadata_path).map_err(|_| {
        format!(
            "refusing to replace non-empty unowned output {}; build-metadata.json is missing",
            output.display()
        )
    })?;
    if !metadata_file.is_file() || metadata_file.file_type().is_symlink() {
        return Err(format!(
            "package ownership metadata is not a regular file: {}",
            metadata_path.display()
        ));
    }
    let text = fs::read_to_string(&metadata_path).map_err(|_| {
        format!(
            "refusing to replace non-empty unowned output {}; build-metadata.json is missing",
            output.display()
        )
    })?;
    let metadata: serde_json::Value = serde_json::from_str(&text).map_err(|error| {
        format!(
            "refusing to replace output with invalid ownership metadata {}: {error}",
            metadata_path.display()
        )
    })?;
    let schema_matches = metadata
        .get("schema_version")
        .and_then(serde_json::Value::as_u64)
        == Some(u64::from(PACKAGE_SCHEMA_VERSION));
    let executable_matches = matches!(
        metadata
            .get("executable")
            .and_then(serde_json::Value::as_str),
        Some("blacksite" | "blacksite.exe")
    );
    if !schema_matches || !executable_matches {
        return Err(format!(
            "refusing to replace output with unrecognized ownership metadata {}",
            metadata_path.display()
        ));
    }
    let executable = output.join(
        metadata
            .get("executable")
            .and_then(serde_json::Value::as_str)
            .unwrap_or_default(),
    );
    let executable_metadata = fs::symlink_metadata(&executable).map_err(|_| {
        format!(
            "owned package executable is missing at {}",
            executable.display()
        )
    })?;
    if !executable_metadata.is_file() || executable_metadata.file_type().is_symlink() {
        return Err(format!(
            "owned package executable is not a regular file: {}",
            executable.display()
        ));
    }
    Ok(())
}

impl Drop for PendingPackage {
    fn drop(&mut self) {
        if !self.published && fs::symlink_metadata(&self.path).is_ok() {
            let _ = remove_path(&self.path);
        }
    }
}

fn seed_directory(source: &Path, destination: &Path) -> Result<(), String> {
    for entry in WalkDir::new(source).follow_links(false) {
        let entry =
            entry.map_err(|error| format!("could not inspect previous package: {error}"))?;
        let relative = entry
            .path()
            .strip_prefix(source)
            .map_err(|error| format!("could not relativize previous package: {error}"))?;
        if relative.as_os_str().is_empty() {
            continue;
        }
        let target = destination.join(relative);
        if entry.file_type().is_symlink() {
            return Err(format!(
                "previous package contains unsupported symlink: {}",
                entry.path().display()
            ));
        }
        if entry.file_type().is_dir() {
            fs::create_dir_all(&target)
                .map_err(|error| format!("could not create {}: {error}", target.display()))?;
        } else if entry.file_type().is_file() {
            if let Some(parent) = target.parent() {
                fs::create_dir_all(parent)
                    .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
            }
            fs::copy(entry.path(), &target).map_err(|error| {
                format!(
                    "could not seed {} from previous package: {error}",
                    target.display()
                )
            })?;
        } else {
            return Err(format!(
                "previous package contains unsupported filesystem entry: {}",
                entry.path().display()
            ));
        }
    }
    Ok(())
}

fn prune_package_root(
    staging: &Path,
    executable_name: &str,
    debug_symbols: Option<&str>,
    stats: &mut CopyStats,
) -> Result<(), String> {
    let entries = fs::read_dir(staging)
        .map_err(|error| format!("could not inspect staged package: {error}"))?;
    for entry in entries {
        let entry = entry.map_err(|error| format!("could not inspect staged package: {error}"))?;
        let name = entry.file_name();
        if name == "assets"
            || name == executable_name
            || name == "build-metadata.json"
            || name == PACKAGE_WORK_MARKER
            || debug_symbols.is_some_and(|debug_symbols| name == debug_symbols)
        {
            continue;
        }
        remove_path(&entry.path())?;
        stats.pruned += 1;
    }
    Ok(())
}

fn remove_path(path: &Path) -> Result<(), String> {
    let metadata = fs::symlink_metadata(path)
        .map_err(|error| format!("could not inspect {}: {error}", path.display()))?;
    if metadata.file_type().is_symlink() {
        return Err(format!(
            "refusing to remove unsupported symlink {}",
            path.display()
        ));
    }
    make_owned_path_writable(path)?;
    if metadata.is_dir() {
        fs::remove_dir_all(path)
    } else {
        fs::remove_file(path)
    }
    .map_err(|error| format!("could not remove {}: {error}", path.display()))
}

#[cfg(windows)]
fn make_owned_path_writable(path: &Path) -> Result<(), String> {
    for entry in WalkDir::new(path).follow_links(false) {
        let entry = entry.map_err(|error| format!("could not inspect owned path: {error}"))?;
        if entry.file_type().is_symlink() {
            return Err(format!(
                "owned package path contains unsupported symlink: {}",
                entry.path().display()
            ));
        }
        let mut permissions = entry
            .metadata()
            .map_err(|error| format!("could not inspect {}: {error}", entry.path().display()))?
            .permissions();
        if permissions.readonly() {
            permissions.set_readonly(false);
            fs::set_permissions(entry.path(), permissions).map_err(|error| {
                format!(
                    "could not make {} writable: {error}",
                    entry.path().display()
                )
            })?;
        }
    }
    Ok(())
}

#[cfg(not(windows))]
fn make_owned_path_writable(_path: &Path) -> Result<(), String> {
    Ok(())
}

fn package_assets(
    project_root: &Path,
    output: &Path,
    default_scene: Option<&str>,
    rendering_tier: RenderingTier,
    stats: &mut CopyStats,
) -> Result<(Vec<ContentEntry>, Vec<ContentEntry>), String> {
    let mut content = Vec::new();
    let mut source_fingerprint = Vec::new();
    let sources = runtime_asset_sources(project_root)?;
    let desired = sources
        .iter()
        .map(|(relative, _)| relative.clone())
        .collect::<BTreeSet<_>>();
    prune_stale_asset_files(output, &desired, stats)?;
    for (relative, source) in sources {
        let relative = relative.as_path();
        let destination = output.join(relative);
        let source_bytes = fs::read(&source)
            .map_err(|error| format!("could not read {}: {error}", source.display()))?;
        source_fingerprint.push(content_entry_bytes(
            &source_bytes,
            &relative.to_string_lossy().replace('\\', "/"),
        ));
        let packaged_bytes = if relative == Path::new("project.ron") {
            packaged_project_settings(&source_bytes, default_scene, rendering_tier)?
        } else {
            source_bytes
        };
        copy_bytes_if_changed(&source, &destination, &packaged_bytes, stats)?;
        content.push(content_entry_bytes(
            &packaged_bytes,
            &format!("assets/{}", relative.to_string_lossy().replace('\\', "/")),
        ));
    }
    prune_empty_asset_directories(output)?;
    content.sort_by(|left, right| left.path.cmp(&right.path));
    source_fingerprint.sort_by(|left, right| left.path.cmp(&right.path));
    Ok((content, source_fingerprint))
}

fn prune_stale_asset_files(
    output: &Path,
    desired: &BTreeSet<PathBuf>,
    stats: &mut CopyStats,
) -> Result<(), String> {
    if !output.exists() {
        return Ok(());
    }
    let mut stale = WalkDir::new(output)
        .follow_links(false)
        .into_iter()
        .map(|entry| entry.map_err(|error| format!("could not inspect staged assets: {error}")))
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .filter(|entry| entry.file_type().is_file())
        .filter_map(|entry| {
            let relative = entry.path().strip_prefix(output).ok()?;
            (!desired.contains(relative)).then(|| entry.path().to_path_buf())
        })
        .collect::<Vec<_>>();
    stale.sort();
    for path in stale {
        remove_path(&path)?;
        stats.pruned += 1;
    }
    Ok(())
}

fn prune_empty_asset_directories(output: &Path) -> Result<(), String> {
    if !output.exists() {
        return Ok(());
    }
    let mut directories = WalkDir::new(output)
        .min_depth(1)
        .follow_links(false)
        .into_iter()
        .map(|entry| entry.map_err(|error| format!("could not inspect staged assets: {error}")))
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .filter(|entry| entry.file_type().is_dir())
        .map(|entry| entry.into_path())
        .collect::<Vec<_>>();
    directories.sort_by_key(|path| std::cmp::Reverse(path.components().count()));
    for directory in directories {
        let is_empty = fs::read_dir(&directory)
            .map_err(|error| format!("could not inspect {}: {error}", directory.display()))?
            .next()
            .is_none();
        if is_empty {
            make_owned_path_writable(&directory)?;
            fs::remove_dir(&directory)
                .map_err(|error| format!("could not prune {}: {error}", directory.display()))?;
        }
    }
    Ok(())
}

fn runtime_asset_sources(project_root: &Path) -> Result<Vec<(PathBuf, PathBuf)>, String> {
    let assets = project_root.join("assets");
    let mut sources = Vec::new();
    let walker = WalkDir::new(&assets)
        .follow_links(false)
        .into_iter()
        .filter_entry(|entry| {
            entry
                .path()
                .strip_prefix(&assets)
                .map(|relative| {
                    relative.as_os_str().is_empty() || !is_excluded_runtime_path(relative)
                })
                .unwrap_or(true)
        });
    for entry in walker {
        let entry = entry.map_err(|error| format!("could not walk runtime assets: {error}"))?;
        let relative = entry
            .path()
            .strip_prefix(&assets)
            .map_err(|error| format!("could not relativize asset path: {error}"))?;
        if entry.file_type().is_symlink() && include_runtime_asset(relative) {
            return Err(format!(
                "runtime asset symlinks are not supported: assets/{}",
                relative.display()
            ));
        }
        if entry.file_type().is_file() && include_runtime_asset(relative) {
            sources.push((relative.to_path_buf(), entry.path().to_path_buf()));
        }
    }
    sources.sort_by(|left, right| left.0.cmp(&right.0));
    Ok(sources)
}

fn fingerprint_runtime_assets(project_root: &Path) -> Result<Vec<ContentEntry>, String> {
    runtime_asset_sources(project_root)?
        .into_iter()
        .map(|(relative, source)| {
            let bytes = fs::read(&source)
                .map_err(|error| format!("could not read {}: {error}", source.display()))?;
            Ok(content_entry_bytes(
                &bytes,
                &relative.to_string_lossy().replace('\\', "/"),
            ))
        })
        .collect()
}

fn include_runtime_asset(relative: &Path) -> bool {
    scene::is_runtime_package_asset(relative)
}

fn is_excluded_runtime_path(relative: &Path) -> bool {
    scene::is_runtime_package_excluded_tree(relative)
}

fn copy_if_changed(source: &Path, destination: &Path, stats: &mut CopyStats) -> Result<(), String> {
    let source_bytes = fs::read(source)
        .map_err(|error| format!("could not read {}: {error}", source.display()))?;
    copy_bytes_if_changed(source, destination, &source_bytes, stats)
}

fn copy_bytes_if_changed(
    source: &Path,
    destination: &Path,
    source_bytes: &[u8],
    stats: &mut CopyStats,
) -> Result<(), String> {
    if fs::read(destination).ok().as_deref() == Some(source_bytes)
        && permissions_match(source, destination)?
    {
        stats.reused += 1;
        return Ok(());
    }
    if let Some(parent) = destination.parent() {
        fs::create_dir_all(parent)
            .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
    }
    if fs::symlink_metadata(destination).is_ok() {
        remove_path(destination)?;
    }
    fs::write(destination, source_bytes)
        .map_err(|error| format!("could not write {}: {error}", destination.display()))?;
    copy_permissions(source, destination)?;
    stats.written += 1;
    Ok(())
}

fn permissions_match(source: &Path, destination: &Path) -> Result<bool, String> {
    let source = fs::metadata(source)
        .map_err(|error| format!("could not inspect {}: {error}", source.display()))?;
    let destination = fs::metadata(destination)
        .map_err(|error| format!("could not inspect {}: {error}", destination.display()))?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        Ok(source.permissions().mode() == destination.permissions().mode())
    }
    #[cfg(not(unix))]
    {
        Ok(source.permissions().readonly() == destination.permissions().readonly())
    }
}

fn copy_permissions(source: &Path, destination: &Path) -> Result<(), String> {
    let permissions = fs::metadata(source)
        .map_err(|error| format!("could not inspect {}: {error}", source.display()))?
        .permissions();
    fs::set_permissions(destination, permissions).map_err(|error| {
        format!(
            "could not set permissions on {}: {error}",
            destination.display()
        )
    })
}

fn content_entry(path: &Path, display_path: &str) -> Result<ContentEntry, String> {
    let bytes =
        fs::read(path).map_err(|error| format!("could not hash {}: {error}", path.display()))?;
    Ok(content_entry_bytes(&bytes, display_path))
}

fn content_entry_bytes(bytes: &[u8], display_path: &str) -> ContentEntry {
    ContentEntry {
        path: display_path.to_string(),
        byte_len: bytes.len() as u64,
        blake3: blake3::hash(bytes).to_hex().to_string(),
    }
}

fn verify_staged_content(staging: &Path, expected: &[ContentEntry]) -> Result<(), String> {
    let expected_paths = expected
        .iter()
        .map(|entry| entry.path.as_str())
        .collect::<BTreeSet<_>>();
    for entry in expected {
        let relative = Path::new(&entry.path);
        if relative.is_absolute()
            || relative.components().any(|component| {
                matches!(
                    component,
                    Component::ParentDir | Component::RootDir | Component::Prefix(_)
                )
            })
        {
            return Err(format!("invalid staged content path `{}`", entry.path));
        }
        let path = staging.join(relative);
        let metadata = fs::symlink_metadata(&path)
            .map_err(|error| format!("staged content is missing at {}: {error}", path.display()))?;
        if !metadata.is_file() || metadata.file_type().is_symlink() {
            return Err(format!(
                "staged content is not a regular file: {}",
                path.display()
            ));
        }
        let actual = content_entry(&path, &entry.path)?;
        if &actual != entry {
            return Err(format!(
                "staged content changed after assembly: {}",
                path.display()
            ));
        }
    }
    for entry in WalkDir::new(staging).follow_links(false) {
        let entry = entry.map_err(|error| format!("could not verify staged package: {error}"))?;
        if entry.file_type().is_symlink() {
            return Err(format!(
                "staged package contains unsupported symlink: {}",
                entry.path().display()
            ));
        }
        if !entry.file_type().is_file() {
            continue;
        }
        let relative = entry
            .path()
            .strip_prefix(staging)
            .map_err(|error| format!("could not relativize staged content: {error}"))?;
        let normalized = relative.to_string_lossy().replace('\\', "/");
        if normalized == PACKAGE_WORK_MARKER || normalized == "build-metadata.json" {
            continue;
        }
        if !expected_paths.contains(normalized.as_str()) {
            return Err(format!(
                "staged package contains untracked file: {}",
                entry.path().display()
            ));
        }
    }
    Ok(())
}

fn packaged_project_settings(
    bytes: &[u8],
    default_scene: Option<&str>,
    rendering_tier: RenderingTier,
) -> Result<Vec<u8>, String> {
    let text = std::str::from_utf8(bytes)
        .map_err(|error| format!("project settings are not UTF-8: {error}"))?;
    let mut settings: settings::ProjectSettings = ron::from_str(text)
        .map_err(|error| format!("could not parse project settings for packaging: {error}"))?;
    if let Some(default_scene) = default_scene {
        settings.default_level = default_scene.to_string();
    }
    if rendering_tier == RenderingTier::Development {
        settings.rendering.gi_mode = settings::GiMode::Forward;
    }
    ron::ser::to_string_pretty(&settings, ron::ser::PrettyConfig::default())
        .map(String::into_bytes)
        .map_err(|error| format!("could not serialize packaged project settings: {error}"))
}

fn write_file_atomically(path: &Path, bytes: &[u8]) -> Result<(), String> {
    let parent = path
        .parent()
        .ok_or_else(|| format!("{} has no parent directory", path.display()))?;
    fs::create_dir_all(parent)
        .map_err(|error| format!("could not create {}: {error}", parent.display()))?;
    let temporary = parent.join(format!(
        ".{}-{}.tmp",
        path.file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("package"),
        std::process::id()
    ));
    fs::write(&temporary, bytes)
        .map_err(|error| format!("could not write {}: {error}", temporary.display()))?;
    if fs::symlink_metadata(path).is_ok() {
        remove_path(path)?;
    }
    fs::rename(&temporary, path).map_err(|error| {
        let _ = fs::remove_file(&temporary);
        format!("could not publish {}: {error}", path.display())
    })
}

fn resolve_project_asset(project_root: &Path, reference: &str) -> Option<PathBuf> {
    let relative = Path::new(reference);
    if relative.is_absolute()
        || relative.components().any(|component| {
            matches!(
                component,
                Component::ParentDir | Component::RootDir | Component::Prefix(_)
            )
        })
    {
        return None;
    }
    let candidate = if relative.starts_with("assets") {
        project_root.join(relative)
    } else {
        project_root.join("assets").join(relative)
    };
    let root = project_root.canonicalize().ok()?;
    let mut existing = candidate.as_path();
    while !existing.exists() {
        existing = existing.parent()?;
    }
    existing
        .canonicalize()
        .ok()
        .filter(|resolved| resolved.starts_with(root))?;
    Some(candidate)
}

fn host_target(project_root: &Path) -> String {
    command_output(project_root, "rustc", &["-vV"])
        .lines()
        .find_map(|line| line.strip_prefix("host: "))
        .unwrap_or("unknown")
        .to_string()
}

fn command_output(project_root: &Path, program: &str, args: &[&str]) -> String {
    command_output_optional(project_root, program, args).unwrap_or_else(|| "unavailable".into())
}

fn command_output_optional(project_root: &Path, program: &str, args: &[&str]) -> Option<String> {
    Command::new(program)
        .current_dir(project_root)
        .args(args)
        .output()
        .ok()
        .filter(|output| output.status.success())
        .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicU64, Ordering};

    static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(1);

    fn fixture_root() -> PathBuf {
        let root = std::env::temp_dir().join(format!(
            "blacksite-package-project-{}-{}",
            std::process::id(),
            NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(root.join("assets/levels")).unwrap();
        fs::create_dir_all(root.join("assets/.index")).unwrap();
        fs::create_dir_all(root.join("assets/build_profiles")).unwrap();
        fs::write(root.join("assets/.index/registry.ron"), "[]").unwrap();
        fs::write(
            root.join("assets/project.ron"),
            "(version:1,template_version:1,default_level:\"assets/levels/main.scn.ron\",asset_roots:[\"assets/levels\"],capabilities:[])",
        )
        .unwrap();
        fs::write(
            root.join("assets/build_profiles/release.ron"),
            "(version:1,name:\"Release\",cargo_profile:Release,target:None,features:[],rendering_tier:\"shipping\",default_scene:None,output_dir:\"dist/release\")",
        )
        .unwrap();
        root
    }

    fn mark_owned_output(output: &Path) {
        if !output.join("blacksite").exists() {
            fs::write(output.join("blacksite"), b"fixture executable").unwrap();
        }
        fs::write(
            output.join("build-metadata.json"),
            r#"{"schema_version":1,"executable":"blacksite"}"#,
        )
        .unwrap();
    }

    #[test]
    fn runtime_asset_filter_excludes_editor_and_source_files() {
        for excluded in [
            ".index/registry.ron",
            ".trash/deleted.png",
            "thumbnails/a.png",
            "build_profiles/release.ron",
            "levels/recovery/a.scn.ron",
            "models/source.blend",
            "textures/a.psd",
            "levels/a.scn.ron.bak",
        ] {
            assert!(!include_runtime_asset(Path::new(excluded)), "{excluded}");
        }
        for included in [
            "project.ron",
            "levels/main.scn.ron",
            "models/mesh.glb",
            "textures/albedo.png",
            "meshes/generated/id.static_mesh.ron",
        ] {
            assert!(include_runtime_asset(Path::new(included)), "{included}");
        }
    }

    #[test]
    fn unchanged_copy_is_reused() {
        let root =
            std::env::temp_dir().join(format!("blacksite-package-copy-{}", std::process::id()));
        fs::create_dir_all(&root).unwrap();
        let source = root.join("source");
        let destination = root.join("destination");
        fs::write(&source, b"same").unwrap();
        let mut stats = CopyStats::default();
        copy_if_changed(&source, &destination, &mut stats).unwrap();
        copy_if_changed(&source, &destination, &mut stats).unwrap();
        assert_eq!(
            stats,
            CopyStats {
                written: 1,
                reused: 1,
                pruned: 0,
            }
        );
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn staged_assets_support_file_directory_shape_changes() {
        let root = fixture_root();
        let output = root.join("staged-assets");
        fs::create_dir_all(&output).unwrap();
        fs::write(output.join("changed"), b"old file").unwrap();
        let desired = BTreeSet::from([PathBuf::from("changed/child.bin")]);
        let mut stats = CopyStats::default();

        prune_stale_asset_files(&output, &desired, &mut stats).unwrap();
        let source = root.join("child.bin");
        fs::write(&source, b"new child").unwrap();
        copy_if_changed(&source, &output.join("changed/child.bin"), &mut stats).unwrap();

        assert_eq!(
            fs::read(output.join("changed/child.bin")).unwrap(),
            b"new child"
        );
        assert_eq!(stats.pruned, 1);

        let replacement = root.join("replacement.bin");
        fs::write(&replacement, b"replacement").unwrap();
        copy_if_changed(&replacement, &output.join("changed"), &mut stats).unwrap();
        assert_eq!(fs::read(output.join("changed")).unwrap(), b"replacement");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn staged_content_verification_detects_tampering_and_untracked_files() {
        let root = fixture_root();
        let staging = root.join("staging");
        fs::create_dir_all(&staging).unwrap();
        let file = staging.join("blacksite");
        fs::write(&file, b"expected").unwrap();
        let expected = vec![content_entry(&file, "blacksite").unwrap()];
        verify_staged_content(&staging, &expected).unwrap();

        fs::write(&file, b"changed").unwrap();
        assert!(verify_staged_content(&staging, &expected)
            .unwrap_err()
            .contains("changed after assembly"));
        fs::write(&file, b"expected").unwrap();
        fs::write(staging.join("unexpected.dll"), b"unexpected").unwrap();
        assert!(verify_staged_content(&staging, &expected)
            .unwrap_err()
            .contains("untracked file"));
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn blocking_content_error_refuses_packaging_before_binary_lookup() {
        let root = fixture_root();
        fs::write(
            root.join("assets/levels/main.scn.ron"),
            r#"(schema_version:2,resources:{},entities:{1:(components:{
                "shared::components::ActorId":("missing-model-owner"),
                "shared::components::ModelRef":(path:"models/missing.glb",scene_index:0),
            })})"#,
        )
        .unwrap();
        let error = run(Options {
            project_root: root.clone(),
            profile_name: "release".into(),
            skip_build: true,
            artifact: None,
        })
        .unwrap_err();
        assert!(error.contains("content validation failed"), "{error}");
        assert!(error.contains("assets/levels/main.scn.ron"), "{error}");
        assert!(error.contains("missing.glb"), "{error}");
        assert!(!root.join("dist/release").exists());
        fs::remove_dir_all(root).unwrap();
    }

    #[cfg(unix)]
    #[test]
    fn package_output_cannot_escape_through_symlink() {
        use std::os::unix::fs::symlink;

        let root = fixture_root();
        let outside = std::env::temp_dir().join(format!(
            "blacksite-package-outside-{}-{}",
            std::process::id(),
            NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(&outside).unwrap();
        fs::create_dir_all(root.join("dist")).unwrap();
        symlink(&outside, root.join("dist/escape")).unwrap();
        let error = resolve_output(&root, "dist/escape/release").unwrap_err();
        assert!(error.contains("symlink"), "{error}");
        fs::remove_dir_all(root).unwrap();
        fs::remove_dir_all(outside).unwrap();
    }

    #[test]
    fn package_output_rejects_source_and_build_trees() {
        let root = fixture_root();
        for output in [
            ".",
            "./assets/export",
            "assets/export",
            "target/export",
            ".git/export",
            "docs/export",
            "crates/export",
            "dist",
        ] {
            assert!(resolve_output(&root, output).is_err(), "{output}");
        }
        fs::remove_dir_all(root).unwrap();
    }

    #[cfg(unix)]
    #[test]
    fn package_output_rejects_symlink_alias_to_assets() {
        use std::os::unix::fs::symlink;

        let root = fixture_root();
        fs::create_dir_all(root.join("dist")).unwrap();
        symlink(root.join("assets"), root.join("dist/assets-link")).unwrap();
        let error = resolve_output(&root, "dist/assets-link/export").unwrap_err();
        assert!(error.contains("project dist directory"), "{error}");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn build_profile_rejects_unsafe_target_triple() {
        let root = fixture_root();
        fs::write(
            root.join("assets/levels/main.scn.ron"),
            "(schema_version:2,resources:{},entities:{})",
        )
        .unwrap();
        let profile: BuildProfile = ron::from_str(
            "(version:1,name:\"Bad\",cargo_profile:Release,target:Some(\"../../tmp\"),features:[],rendering_tier:\"shipping\",default_scene:Some(\"assets/levels/main.scn.ron\"),output_dir:\"dist/release\")",
        )
        .unwrap();
        assert!(validate_profile(&root, &profile)
            .unwrap_err()
            .contains("target triple"));
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn build_profile_rejects_unsupported_platform_bundle() {
        let root = fixture_root();
        let profile: BuildProfile = ron::from_str(
            "(version:1,name:\"Web\",cargo_profile:Release,target:Some(\"wasm32-unknown-unknown\"),features:[],rendering_tier:\"shipping\",default_scene:None,output_dir:\"dist/web\")",
        )
        .unwrap();
        let error = validate_profile(&root, &profile).unwrap_err();
        assert!(error.contains("native package layout"), "{error}");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn windows_msvc_qa_requires_and_selects_the_cargo_pdb() {
        let root = fixture_root();
        let executable = root.join("game.exe");
        let pdb = root.join("game.pdb");
        fs::write(&executable, b"exe").unwrap();
        fs::write(&pdb, b"symbols").unwrap();
        let profile: BuildProfile = ron::from_str(
            "(version:1,name:\"QA\",cargo_profile:Qa,target:Some(\"x86_64-pc-windows-msvc\"),features:[],rendering_tier:\"representative\",default_scene:None,output_dir:\"dist/qa\")",
        )
        .unwrap();
        let with_symbols = BuiltArtifacts {
            executable: executable.clone(),
            companions: vec![pdb.clone()],
        };
        assert_eq!(
            select_debug_symbols(&profile, "x86_64-pc-windows-msvc", &with_symbols).unwrap(),
            Some((pdb, "blacksite.pdb"))
        );

        let without_symbols = BuiltArtifacts {
            executable,
            companions: Vec::new(),
        };
        assert!(
            select_debug_symbols(&profile, "x86_64-pc-windows-msvc", &without_symbols)
                .unwrap_err()
                .contains("PDB")
        );
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn build_profile_default_scene_must_pass_scene_validation() {
        let root = fixture_root();
        fs::create_dir_all(root.join("assets/levels/custom")).unwrap();
        fs::write(
            root.join("assets/levels/custom/bad.scn.ron"),
            "(schema_version:999,resources:{},entities:{})",
        )
        .unwrap();
        let profile: BuildProfile = ron::from_str(
            "(version:1,name:\"Bad Scene\",cargo_profile:Release,target:None,features:[],rendering_tier:\"shipping\",default_scene:Some(\"assets/levels/custom/bad.scn.ron\"),output_dir:\"dist/release\")",
        )
        .unwrap();
        let error = validate_profile(&root, &profile).unwrap_err();
        assert!(error.contains("default scene"), "{error}");
        assert!(error.contains("schema"), "{error}");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn build_profile_default_scene_cannot_use_an_excluded_tree() {
        let root = fixture_root();
        fs::write(
            root.join("assets/build_profiles/hidden.scn.ron"),
            "(schema_version:2,resources:{},entities:{})",
        )
        .unwrap();
        let profile: BuildProfile = ron::from_str(
            "(version:1,name:\"Excluded\",cargo_profile:Release,target:None,features:[],rendering_tier:\"shipping\",default_scene:Some(\"assets/build_profiles/hidden.scn.ron\"),output_dir:\"dist/release\")",
        )
        .unwrap();
        let error = validate_profile(&root, &profile).unwrap_err();
        assert!(error.contains("assets/levels"), "{error}");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn packaged_project_settings_apply_profile_default_scene() {
        let root = fixture_root();
        let path = root.join("full-project.ron");
        let source = settings::ProjectSettings {
            default_level: "assets/levels/old.scn.ron".into(),
            ..Default::default()
        };
        fs::write(
            &path,
            ron::ser::to_string_pretty(&source, ron::ser::PrettyConfig::default()).unwrap(),
        )
        .unwrap();
        let source_bytes = fs::read(&path).unwrap();
        let bytes = packaged_project_settings(
            &source_bytes,
            Some("assets/levels/profile-default.scn.ron"),
            RenderingTier::Development,
        )
        .unwrap();
        let packaged: settings::ProjectSettings =
            ron::from_str(std::str::from_utf8(&bytes).unwrap()).unwrap();
        assert_eq!(
            packaged.default_level,
            "assets/levels/profile-default.scn.ron"
        );
        assert_eq!(packaged.rendering.gi_mode, settings::GiMode::Forward);
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn failed_staging_does_not_modify_previous_package() {
        let root = fixture_root();
        let output = root.join("dist/release");
        fs::create_dir_all(&output).unwrap();
        fs::write(output.join("blacksite"), b"old").unwrap();
        mark_owned_output(&output);
        let source = root.join("new-game");
        fs::write(&source, b"new").unwrap();

        let staging_path;
        {
            let pending = PendingPackage::new(&output).unwrap();
            staging_path = pending.path().to_path_buf();
            pending.seed_from(&output).unwrap();
            copy_if_changed(
                &source,
                &pending.path().join("blacksite"),
                &mut CopyStats::default(),
            )
            .unwrap();
        }

        assert_eq!(fs::read(output.join("blacksite")).unwrap(), b"old");
        assert!(!staging_path.exists());
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn publishing_replaces_complete_package_and_removes_staging() {
        let root = fixture_root();
        let output = root.join("dist/release");
        fs::create_dir_all(&output).unwrap();
        fs::write(output.join("obsolete"), b"old").unwrap();
        mark_owned_output(&output);
        let mut pending = PendingPackage::new(&output).unwrap();
        let staging_path = pending.path().to_path_buf();
        fs::write(pending.path().join("blacksite"), b"new").unwrap();
        mark_owned_output(pending.path());

        pending.publish(&output).unwrap();

        assert_eq!(fs::read(output.join("blacksite")).unwrap(), b"new");
        assert!(!output.join("obsolete").exists());
        assert!(!staging_path.exists());
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn nonempty_unowned_output_is_never_replaced() {
        let root = fixture_root();
        let output = root.join("dist/release");
        fs::create_dir_all(&output).unwrap();
        fs::write(output.join("user-notes.txt"), b"keep").unwrap();

        let error = PendingPackage::new(&output).err().unwrap();

        assert!(error.contains("unowned output"), "{error}");
        assert_eq!(fs::read(output.join("user-notes.txt")).unwrap(), b"keep");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn package_lock_prevents_concurrent_publication_and_releases_cleanly() {
        let root = fixture_root();
        let output = root.join("dist/release");
        let lock = PackageLock::acquire(&output).unwrap();

        let error = PackageLock::acquire(&output).err().unwrap();
        assert!(error.contains("another build may be active"), "{error}");
        drop(lock);

        PackageLock::acquire(&output).unwrap();
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn unrecognized_lock_file_is_preserved_and_rejected() {
        let root = fixture_root();
        let output = root.join("dist/release");
        fs::create_dir_all(root.join("dist")).unwrap();
        let lock_path = root.join("dist/.blacksite-release.lock");
        fs::write(&lock_path, b"user notes").unwrap();

        let error = PackageLock::acquire(&output).err().unwrap();

        assert!(error.contains("unrecognized package lock"), "{error}");
        assert_eq!(fs::read(&lock_path).unwrap(), b"user notes");
        fs::remove_dir_all(root).unwrap();
    }

    #[cfg(unix)]
    #[test]
    fn symlinked_lock_file_never_truncates_its_target() {
        use std::os::unix::fs::symlink;

        let root = fixture_root();
        let output = root.join("dist/release");
        fs::create_dir_all(root.join("dist")).unwrap();
        let outside = root.join("user-file");
        fs::write(&outside, b"keep me").unwrap();
        symlink(&outside, root.join("dist/.blacksite-release.lock")).unwrap();

        let error = PackageLock::acquire(&output).err().unwrap();

        assert!(error.contains("symlinked package lock"), "{error}");
        assert_eq!(fs::read(&outside).unwrap(), b"keep me");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn interrupted_staging_is_removed_and_backup_is_restored() {
        let root = fixture_root();
        let output = root.join("dist/release");
        fs::create_dir_all(root.join("dist")).unwrap();
        let staging = root.join("dist/.blacksite-release-1-2-0.staging");
        fs::create_dir_all(&staging).unwrap();
        write_work_marker(&staging, &output).unwrap();
        fs::write(staging.join("partial"), b"partial").unwrap();
        let backup = root.join("dist/.blacksite-release-1-2-0.backup");
        fs::create_dir_all(&backup).unwrap();
        fs::write(backup.join("blacksite"), b"previous").unwrap();
        mark_owned_output(&backup);

        recover_interrupted_package(&output).unwrap();

        assert!(!staging.exists());
        assert!(!backup.exists());
        assert_eq!(fs::read(output.join("blacksite")).unwrap(), b"previous");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn unowned_staging_directory_is_never_scavenged() {
        let root = fixture_root();
        let output = root.join("dist/release");
        fs::create_dir_all(root.join("dist")).unwrap();
        let staging = root.join("dist/.blacksite-release-notes.staging");
        fs::create_dir_all(&staging).unwrap();
        fs::write(staging.join("notes.txt"), b"keep").unwrap();

        let error = recover_interrupted_package(&output).unwrap_err();

        assert!(error.contains("unowned staging"), "{error}");
        assert_eq!(fs::read(staging.join("notes.txt")).unwrap(), b"keep");
        fs::remove_dir_all(root).unwrap();
    }

    #[cfg(unix)]
    #[test]
    fn runtime_asset_symlinks_are_rejected() {
        use std::os::unix::fs::symlink;

        let root = fixture_root();
        let outside = root.join("outside.bin");
        fs::write(&outside, b"outside").unwrap();
        symlink(&outside, root.join("assets/external.bin")).unwrap();
        let error = runtime_asset_sources(&root).unwrap_err();
        assert!(error.contains("symlinks are not supported"), "{error}");
        fs::remove_dir_all(root).unwrap();
    }
}
