use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use sculblog::html::{strip_and_extract_links, write_page_html};
use sculblog::xanadoc::{generate_xvuid, write_xanadoc_dif};
use std::os::unix::ffi::OsStrExt;

fn filename_map_path(maps_path: &Path) -> PathBuf {
    maps_path.join("filename_to_xvuid.map")
}

fn canonicalize_file_path(file_path: &str) -> String {
    let expanded = sculblog::html::expand_tilde_path(file_path);
    match std::fs::canonicalize(&expanded) {
        Ok(p) => p.to_string_lossy().to_string(),
        Err(_) => expanded,
    }
}

fn lookup_filename_xvuid(maps_path: &Path, canonical_path: &str) -> Option<String> {
    let map_path = filename_map_path(maps_path);
    let content = fs::read_to_string(&map_path).ok()?;
    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() { continue; }
        if let Some((path, xvuid)) = line.split_once('\t') {
            if path == canonical_path {
                return Some(xvuid.to_string());
            }
        }
    }
    None
}

fn save_filename_xvuid(maps_path: &Path, canonical_path: &str, xvuid_hex: &str) {
    let map_path = filename_map_path(maps_path);
    let content = fs::read_to_string(&map_path).unwrap_or_default();
    let mut content = filter_filename_map(&content, canonical_path);
    content.push_str(&format!("{}\t{}\n", canonical_path, xvuid_hex));
    let _ = fs::write(&map_path, content);
}

fn remove_filename_entry(maps_path: &Path, canonical_path: &str) {
    let map_path = filename_map_path(maps_path);
    let content = match fs::read_to_string(&map_path) {
        Ok(c) => c,
        Err(_) => return,
    };
    let _ = fs::write(&map_path, filter_filename_map(&content, canonical_path));
}

fn get_word_count(md: &str) -> usize {
    md.split_whitespace().count()
}

/// Reference record from xdc DTOB (decoded from ffi_xdc_decode_references flat buffer)
struct XdcRef {
    kind: u8,        // 0=transclusion, 1=microlink, 2=macrolink
    def_cid: [u8; 32],
    ref_start: u32,
    length: u32,
}

fn read_xdc_content(file_path: &str) -> Option<Vec<u8>> {
    let data = fs::read(file_path).ok()?;
    unsafe {
        let mut out_len: usize = 0;
        let ptr = sculblog::dtob_ffi::ffi_xdc_decode_content(
            data.as_ptr(), data.len(), &mut out_len
        );
        if ptr.is_null() || out_len == 0 { return None; }
        let result = std::slice::from_raw_parts(ptr, out_len).to_vec();
        sculblog::dtob_ffi::ffi_xdc_decode_content as usize; // just to use it
        libc::free(ptr as *mut libc::c_void);
        Some(result)
    }
}

fn read_xdc_references(file_path: &str) -> Vec<XdcRef> {
    let data = match fs::read(file_path) {
        Ok(d) => d,
        Err(_) => return Vec::new(),
    };
    unsafe {
        let mut out_len: usize = 0;
        let ptr = sculblog::dtob_ffi::ffi_xdc_decode_references(
            data.as_ptr(), data.len(), &mut out_len
        );
        if ptr.is_null() || out_len < 4 { return Vec::new(); }
        let buf = std::slice::from_raw_parts(ptr, out_len);

        let count = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
        let mut refs = Vec::with_capacity(count);
        let mut off = 4;

        for _ in 0..count {
            if off + 41 > out_len { break; }
            let kind = buf[off]; off += 1;
            let mut def_cid = [0u8; 32];
            def_cid.copy_from_slice(&buf[off..off+32]); off += 32;
            let ref_start = u32::from_le_bytes([buf[off], buf[off+1], buf[off+2], buf[off+3]]); off += 4;
            let length = u32::from_le_bytes([buf[off], buf[off+1], buf[off+2], buf[off+3]]); off += 4;
            refs.push(XdcRef { kind, def_cid, ref_start, length });
        }

        libc::free(ptr as *mut libc::c_void);
        refs
    }
}

/// Inject <a> tags into markdown content at reference byte positions.
/// Tags are inserted in reverse order to preserve earlier byte offsets.
fn inject_reference_links(md: &[u8], refs: &[XdcRef]) -> String {
    let mut sorted: Vec<&XdcRef> = refs.iter().collect();
    sorted.sort_by(|a, b| b.ref_start.cmp(&a.ref_start)); // reverse order

    let mut result = md.to_vec();
    for r in sorted {
        let start = r.ref_start as usize;
        let end = start + r.length as usize;
        if end > result.len() { continue; }

        let class = match r.kind {
            0 => "transclusion",
            1 => "microlink",
            2 => "macrolink",
            _ => "link",
        };
        let cid_hex = hex::encode(r.def_cid);
        let open_tag = format!("<a href=\"https://xanadex.com/doc?cid={}\" class=\"{}\">", cid_hex, class);
        let close_tag = b"</a>";

        result.splice(end..end, close_tag.iter().copied());
        result.splice(start..start, open_tag.as_bytes().iter().copied());
    }

    String::from_utf8_lossy(&result).to_string()
}

fn check_writable(dir: &Path, name: &str) {
    if !dir.exists() {
        if let Err(e) = std::fs::create_dir_all(dir) {
            eprintln!("Error: Cannot create {} directory at {}: {}", name, dir.display(), e);
            eprintln!("To fix this, please run:");
            eprintln!("  sudo mkdir -p {}", dir.display());
            eprintln!("  sudo chown -R $USER {}", dir.display());
            std::process::exit(2);
        }
    }
    
    let c_path = std::ffi::CString::new(dir.as_os_str().as_bytes()).unwrap();
    let res = unsafe { libc::access(c_path.as_ptr(), libc::W_OK) };
    if res != 0 {
        eprintln!("Error: Insufficient permissions to write to {} at {}", name, dir.display());
        eprintln!("To fix this, please run:");
        eprintln!("  sudo chown -R $USER {}", dir.display());
        eprintln!("  chmod u+w {}", dir.display());
        std::process::exit(2);
    }
}

use sculblog::shared::{load_config, filter_filename_map};

fn post_updater(file_path: &str, subcommand: &str, xvuid_flag: Option<&str>, debug: bool, force_title_prompt: bool) {
    if !Path::new(file_path).is_file() {
        eprintln!("Error: Target file '{}' does not exist or is not a regular file.", file_path);
        std::process::exit(1);
    }

    let repo_path = load_config("REPO_DIR");
    check_writable(&repo_path, "REPO_DIR");
    let maps_path = load_config("MAPS_DIR");
    let canonical = canonicalize_file_path(file_path);

    let is_xdc = file_path.ends_with(".xdc");

    let (md, page_html): (String, String);

    if is_xdc {
        // Read markdown content from xdc DTOB
        let content_bytes = read_xdc_content(file_path).unwrap_or_else(|| {
            eprintln!("Error: Failed to decode xdc content from '{}'", file_path);
            std::process::exit(1);
        });
        md = String::from_utf8_lossy(&content_bytes).to_string();

        // Read references and inject <a> tags into markdown
        let refs = read_xdc_references(file_path);
        let md_with_links = if refs.is_empty() {
            md.clone()
        } else {
            inject_reference_links(&content_bytes, &refs)
        };

        // Write modified markdown to temp file for pandoc
        let tmp = tempfile::NamedTempFile::new().expect("Failed to create temp file");
        fs::write(tmp.path(), &md_with_links).expect("Failed to write temp file");

        let lua_filter = std::env::current_exe().ok()
            .and_then(|exe| exe.parent().and_then(|p| p.parent()).and_then(|p| p.parent()).map(|d| d.join("subsection-wrapper.lua")))
            .filter(|p| p.is_file());
        let lua_str = lua_filter.as_ref().map(|p| p.to_string_lossy().to_string());
        page_html = write_page_html(tmp.path().to_str().unwrap(), lua_str.as_deref(), debug);
    } else {
        let lua_filter = std::env::current_exe().ok()
            .and_then(|exe| exe.parent().and_then(|p| p.parent()).and_then(|p| p.parent()).map(|d| d.join("subsection-wrapper.lua")))
            .filter(|p| p.is_file());
        if lua_filter.is_none() {
            eprintln!("warning: subsection-wrapper.lua not found next to binary, skipping lua filter");
        }
        let lua_str = lua_filter.as_ref().map(|p| p.to_string_lossy().to_string());
        page_html = write_page_html(file_path, lua_str.as_deref(), debug);
        md = fs::read_to_string(file_path).unwrap_or_default();
    }

    let word_count = get_word_count(&md);

    let path = Path::new(file_path);
    let _file_name = path.file_stem().unwrap().to_str().unwrap();

    // For xdc files, links are baked into the HTML — pass empty refs
    let (stripped_html, macrolinks, autolinks) = if is_xdc {
        (page_html.clone(), Vec::new(), Vec::new())
    } else {
        strip_and_extract_links(&page_html)
    };
    
    let xvuid;
    let is_new;
    
    // Priority: --xvuid flag > filename map lookup > generate new
    let resolved_xvuid_str: Option<String> = if let Some(x_str) = xvuid_flag {
        Some(x_str.to_string())
    } else {
        lookup_filename_xvuid(&maps_path, &canonical)
    };
    
    if let Some(x_str) = resolved_xvuid_str {
        let mut arr = [0u8; 32];
        if hex::decode_to_slice(&x_str, &mut arr).is_ok() {
            let dif_path = repo_path.join(format!("{}.dif", x_str));
            if !dif_path.exists() {
                eprintln!("Error: Resolved xvuid {} does not have a .dif file. Generating new xvuid.", x_str);
                xvuid = generate_xvuid();
                is_new = true;
            } else {
                xvuid = arr;
                is_new = false;
            }
        } else {
            eprintln!("Invalid xvuid format: {}", x_str);
            std::process::exit(1);
        }
    } else {
        xvuid = generate_xvuid();
        is_new = true;
    }
    
    let title = if is_new || force_title_prompt {
        use std::io::{self, Write};
        print!("What is the header of the page? ");
        io::stdout().flush().unwrap();
        let mut t = String::new();
        io::stdin().read_line(&mut t).unwrap();
        let trimmed = t.trim();
        let mut byte_count = 0;
        let mut res = String::new();
        for c in trimmed.chars() {
            let len = c.len_utf8();
            if byte_count + len > 140 { break; }
            byte_count += len;
            res.push(c);
        }
        res
    } else {
        let maps_path = load_config("MAPS_DIR");
        let cache = sculblog::maps::MapsCache::load(&maps_path);
        cache.lookup_title(&hex::encode(xvuid))
            .unwrap_or_else(|| "Untitled".to_string())
    };
    
    let doc_timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
    
    match subcommand {
        "draft" => {
            let maps_path = load_config("MAPS_DIR");
            check_writable(&maps_path, "MAPS_DIR");

            let md_repo_path = repo_path.join("md");
            if !md_repo_path.exists() {
                let _ = std::fs::create_dir_all(&md_repo_path);
            }

            let _ = write_xanadoc_dif(
                &md_repo_path,
                &maps_path,
                "",
                "",
                xvuid,
                &title,
                word_count,
                &md,
                Vec::new(),
                Vec::new(),
                doc_timestamp,
                debug,
                false,
            );

            let res = write_xanadoc_dif(
                &repo_path,
                &maps_path,
                "",
                "",
                xvuid,
                &title,
                word_count,
                &stripped_html,
                macrolinks,
                autolinks,
                doc_timestamp,
                debug,
                true,
            );
            if let Err(e) = res {
                eprintln!("Error writing xanadoc dif: {}", e);
            } else {
                let xvuid_hex = hex::encode(xvuid);
                let current_mapping = lookup_filename_xvuid(&maps_path, &canonical);
                if current_mapping.as_deref() != Some(&xvuid_hex) {
                    save_filename_xvuid(&maps_path, &canonical, &xvuid_hex);
                    eprintln!("Saved filename mapping: {} -> {}", canonical, xvuid_hex);
                }
                println!("xvuid: {}", xvuid_hex);
            }
        }
        _ => eprintln!("unknown subcommand {}", subcommand),
    }
}

fn main() {
    unsafe {
        sculblog::dtob_ffi::ffi_bromberg_init();
    }
    
    let args: Vec<String> = env::args().collect();

    let debug = args.iter().any(|s| s == "--debug");
    let force_title_prompt = args.iter().any(|s| s == "--title");

    // Parse --xvuid <hex> flag
    let xvuid_flag: Option<String> = {
        let mut val = None;
        for i in 1..args.len() {
            if args[i] == "--xvuid" && i + 1 < args.len() {
                val = Some(args[i + 1].clone());
                break;
            }
        }
        val
    };

    // Build positional args, skipping --flags and the value after --xvuid
    let positional: Vec<&str> = {
        let mut result = Vec::new();
        let mut skip_next = false;
        for s in &args[1..] {
            if skip_next {
                skip_next = false;
                continue;
            }
            if s == "--xvuid" {
                skip_next = true;
                continue;
            }
            if s.starts_with("--") {
                continue;
            }
            result.push(s.as_str());
        }
        result
    };

    if positional.is_empty() {
        eprintln!("Usage: sculblog <command> [args...]");
        eprintln!("Commands:");
        eprintln!("  draft <file_path> [--xvuid <xvuid>]");
        eprintln!("  cache-maps");
        eprintln!("  serve:xvuid <xvuid> [--recent | <offset>]");
        eprintln!("  table");
        eprintln!("  resolve:ccid:xvuid <ccid>");
        eprintln!("  resolve:cid:xvuid <cid>");
        eprintln!("  resolve:ccid:cid <ccid>");
        eprintln!("  resolve:xvuid:cid <xvuid>");
        eprintln!("  resolve:xvuid:ccid <xvuid>");
        eprintln!("  resolve:ccid:title <ccid>");
        eprintln!("  rm:xvuid <xvuid>");
        eprintln!("  rm:cid <cid>");
        eprintln!("  rm:ccid <ccid>");
        eprintln!("  rm:filename <file_path>");
        eprintln!("  daemon");
        std::process::exit(1);
    }

    let command = positional[0];

    match command {
        "daemon" => {
            sculblog::daemon::run_daemon();
        }
        "draft" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog draft <file_path> [--xvuid <xvuid>]");
                std::process::exit(1);
            }
            let file_path = positional[1];
            post_updater(file_path, command, xvuid_flag.as_deref(), debug, force_title_prompt);
        }
        "cache-maps" => {
            let repo_path = load_config("REPO_DIR");
            let maps_path = load_config("MAPS_DIR");
            check_writable(&maps_path, "MAPS_DIR");
            sculblog::maps::rebuild_memcache_maps(&repo_path, &maps_path);
        }
        "serve:xvuid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog serve:xvuid <xvuid> [--recent | <offset>]");
                std::process::exit(1);
            }
            let xvuid = positional[1];
            let is_recent = args.iter().any(|s| s == "--recent");
            let requested_offset = if is_recent {
                None
            } else if positional.len() >= 3 {
                positional[2].parse().ok()
            } else {
                None
            };
            
            let maps_path = load_config("MAPS_DIR");
            let cache = sculblog::maps::MapsCache::load(&maps_path);
            
            let (target_offset, snapshot_offset) = if let Some(off) = requested_offset {
                let snapshot_index = (off / sculblog::SNAPSHOT_INTERVAL) * sculblog::SNAPSHOT_INTERVAL;
                (Some(off), cache.lookup_snapshot_offset(xvuid, snapshot_index))
            } else if let Some((latest_index, latest_snapshot_offset)) =
                cache.lookup_latest(xvuid)
            {
                (Some(latest_index), Some(latest_snapshot_offset))
            } else {
                eprintln!("warning: maps cache has no entry for xvuid {} (try running `sculblog cache-maps` first)", xvuid);
                (None, None)
            };

            let repo_path = load_config("REPO_DIR");
            let dif_path = repo_path.join(format!("{}.dif", xvuid));
            
            if let Some(built) = sculblog::serve::build_content_from_snapshot_offset(&dif_path, target_offset, snapshot_offset, debug) {
                use std::io::Write;
                let _ = std::io::stdout().write_all(&built.current_content);
            } else {
                eprintln!("Failed to serve reconstructed bytes for xvuid: {}", xvuid);
                eprintln!("  dif_path: {:?} (exists: {}, size: {})",
                    dif_path,
                    dif_path.exists(),
                    std::fs::metadata(&dif_path).map(|m| m.len()).unwrap_or(0)
                );
                eprintln!("  target_offset: {:?}", target_offset);
                eprintln!("  snapshot_offset: {:?}", snapshot_offset);
                std::process::exit(1);
            }
        }
        "serve:cid" | "serve:ccid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog {} <hash>", command);
                std::process::exit(1);
            }
            let hash = positional[1];
            let maps_path = load_config("MAPS_DIR");
            let cache = sculblog::maps::MapsCache::load(&maps_path);
            let map_name = if command == "serve:cid" { "cid_to_xvuid" } else { "ccid_to_xvuid" };

            match cache.lookup_hash(map_name, hash) {
                sculblog::maps::HashLookup::Found { xvuid, index } => {
                    let snapshot_index = (index / sculblog::SNAPSHOT_INTERVAL) * sculblog::SNAPSHOT_INTERVAL;
                    let snapshot_offset = cache.lookup_snapshot_offset(&xvuid, snapshot_index);
                    let repo_path = load_config("REPO_DIR");
                    let dif_path = repo_path.join(format!("{}.dif", xvuid));
                    
                    if let Some(built) = sculblog::serve::build_content_from_snapshot_offset(
                        &dif_path,
                        Some(index),
                        snapshot_offset,
                        debug,
                    ) {
                        use std::io::Write;
                        let _ = std::io::stdout().write_all(&built.current_content);
                    } else {
                        eprintln!("Failed to serve reconstructed bytes for {} hash: {}", command, hash);
                        eprintln!("  resolved xvuid: {}", xvuid);
                        eprintln!("  dif_path: {:?} (exists: {}, size: {})",
                            dif_path,
                            dif_path.exists(),
                            std::fs::metadata(&dif_path).map(|m| m.len()).unwrap_or(0)
                        );
                        eprintln!("  target index: {}", index);
                        eprintln!("  snapshot_offset: {:?}", snapshot_offset);
                        std::process::exit(1);
                    }
                }
                sculblog::maps::HashLookup::Missing => {
                    eprintln!("Could not map {} {} to any known offset", command, hash);
                    std::process::exit(1);
                }
            }
        }
        "resolve:cid:xvuid" | "resolve:ccid:xvuid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog {} <hash>", command);
                std::process::exit(1);
            }
            let hash = positional[1];
            let maps_path = load_config("MAPS_DIR");
            let cache = sculblog::maps::MapsCache::load(&maps_path);
            let map_name = if command == "resolve:cid:xvuid" { "cid_to_xvuid" } else { "ccid_to_xvuid" };

            match cache.lookup_hash(map_name, hash) {
                sculblog::maps::HashLookup::Found { xvuid, index: _ } => {
                    println!("{}", xvuid);
                }
                sculblog::maps::HashLookup::Missing => {
                    std::process::exit(1);
                }
            }
        }
        "resolve:ccid:cid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog resolve:ccid:cid <ccid>");
                std::process::exit(1);
            }
            let hash = positional[1];
            let maps_path = load_config("MAPS_DIR");
            let cache = sculblog::maps::MapsCache::load(&maps_path);

            match cache.lookup_hash("ccid_to_xvuid", hash) {
                sculblog::maps::HashLookup::Found { xvuid, index } => {
                    let snapshot_index = (index / sculblog::SNAPSHOT_INTERVAL) * sculblog::SNAPSHOT_INTERVAL;
                    let snapshot_offset = cache.lookup_snapshot_offset(&xvuid, snapshot_index);
                    let repo_path = load_config("REPO_DIR");
                    let dif_path = repo_path.join(format!("{}.dif", xvuid));
                    
                    if let Some(built) = sculblog::serve::build_content_from_snapshot_offset(&dif_path, Some(index), snapshot_offset, false) {
                        if let Some(ids) = xanadoc::decode_ids(&built.current_content) {
                            println!("{}", hex::encode(ids.cid));
                        }
                        unsafe { sculblog::dtob_ffi::ffi_dtob_free(built.target_root); }
                    } else {
                        eprintln!("Failed to parse target dif structure.");
                        std::process::exit(1);
                    }
                }
                sculblog::maps::HashLookup::Missing => {
                    std::process::exit(1);
                }
            }
        }
        "resolve:xvuid:cid" | "resolve:xvuid:ccid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog {} <xvuid>", command);
                std::process::exit(1);
            }
            let xvuid = positional[1];
            let maps_path = load_config("MAPS_DIR");
            let cache = sculblog::maps::MapsCache::load(&maps_path);
            let field = if command == "resolve:xvuid:cid" { 1usize } else { 2usize };
            match cache.lookup_meta_hex(xvuid, field) {
                Some(val) => println!("{}", val),
                None => std::process::exit(1),
            }
        }
        "resolve:ccid:title" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog resolve:ccid:title <ccid>");
                std::process::exit(1);
            }
            let hash = positional[1];
            let maps_path = load_config("MAPS_DIR");
            let cache = sculblog::maps::MapsCache::load(&maps_path);
            match cache.lookup_hash("ccid_to_xvuid", hash) {
                sculblog::maps::HashLookup::Found { xvuid, .. } => {
                    match cache.lookup_title(&xvuid) {
                        Some(title) => println!("{}", title),
                        None => std::process::exit(1),
                    }
                }
                sculblog::maps::HashLookup::Missing => std::process::exit(1),
            }
        }
        "table" => {
            let maps_path = load_config("MAPS_DIR");
            sculblog::table::generate_table(&maps_path);
        }
        "table:xvuid" | "table:cid" | "table:ccid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog {} <hash>", command);
                std::process::exit(1);
            }
            let hash = positional[1];
            let maps_path = load_config("MAPS_DIR");
            let cache = sculblog::maps::MapsCache::load(&maps_path);
            
            let target_xvuid = if command == "table:xvuid" {
                hash.to_string()
            } else {
                let map_name = if command == "table:cid" { "cid_to_xvuid" } else { "ccid_to_xvuid" };
                match cache.lookup_hash(map_name, hash) {
                    sculblog::maps::HashLookup::Found { xvuid, .. } => xvuid,
                    sculblog::maps::HashLookup::Missing => {
                        eprintln!("Could not map {} {} to any known xvuid", command, hash);
                        std::process::exit(1);
                    }
                }
            };
            
            sculblog::table::generate_table_for_xvuid(&maps_path, &cache, &target_xvuid);
        }
        "rm:xvuid" | "remove:xvuid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog rm:xvuid <xvuid>");
                std::process::exit(1);
            }
            let xvuid = positional[1];
            let repo_path = load_config("REPO_DIR");
            let maps_path = load_config("MAPS_DIR");
            sculblog::remove::remove_xvuid(&repo_path, &maps_path, xvuid);
            
            // Re-use helper to remove any filename mapping that points to this xvuid
            // (technically this requires finding it first, since the helper takes canonical_path)
            let map_path = filename_map_path(&maps_path);
            if let Ok(content) = fs::read_to_string(&map_path) {
                for line in content.lines() {
                    if let Some((path, v)) = line.split_once('\t') {
                        if v == xvuid {
                            remove_filename_entry(&maps_path, path);
                        }
                    }
                }
            }
        }
        "rm:cid" | "remove:cid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog rm:cid <cid>");
                std::process::exit(1);
            }
            let hash = positional[1];
            let repo_path = load_config("REPO_DIR");
            let maps_path = load_config("MAPS_DIR");
            sculblog::remove::remove_by_hash(&repo_path, &maps_path, "cid_to_xvuid", hash);
        }
        "rm:ccid" | "remove:ccid" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog rm:ccid <ccid>");
                std::process::exit(1);
            }
            let hash = positional[1];
            let repo_path = load_config("REPO_DIR");
            let maps_path = load_config("MAPS_DIR");
            sculblog::remove::remove_by_hash(&repo_path, &maps_path, "ccid_to_xvuid", hash);
        }
        "rm:filename" | "remove:filename" => {
            if positional.len() < 2 {
                eprintln!("Usage: sculblog rm:filename <file_path>");
                std::process::exit(1);
            }
            let file_path = positional[1];
            let canonical = canonicalize_file_path(file_path);
            let maps_path = load_config("MAPS_DIR");
            let repo_path = load_config("REPO_DIR");
            match lookup_filename_xvuid(&maps_path, &canonical) {
                Some(xvuid) => {
                    sculblog::remove::remove_xvuid(&repo_path, &maps_path, &xvuid);
                    remove_filename_entry(&maps_path, &canonical);
                    eprintln!("Removed filename mapping: {}", canonical);
                }
                None => {
                    eprintln!("Error: No xvuid mapping found for {}", canonical);
                    std::process::exit(1);
                }
            }
        }
        _ => {
            eprintln!("Invalid command: {}", command);
            std::process::exit(1);
        }
    }
}
