//! Deterministic and bounded tar/OAR archive handling.

// Public signatures mirror the fixed C# compatibility surface. Several own
// inputs intentionally and RegionSettings intentionally exposes boolean fields.
#![allow(clippy::pedantic)]

use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Component, Path, PathBuf};
use std::sync::Mutex;

use flate2::{Compression, GzBuilder, read::GzDecoder};
use libremetaverse_types::compat::ReadWrite;
use libremetaverse_types::{AssetType, Error, UUID};

use crate::assets::TarArchiveReaderTarEntryType;

const BLOCK: usize = 512;
const MAX_ENTRY_BYTES: usize = 64 * 1024 * 1024;
const MAX_TOTAL_BYTES: usize = 1024 * 1024 * 1024;
const MAX_ENTRIES: usize = 100_000;
const MAX_PATH_BYTES: usize = 4096;
const MAX_PATH_DEPTH: usize = 64;
const ARCHIVE_XML: &str = "<?xml version=\"1.0\" encoding=\"utf-16\"?>\n<archive major_version=\"0\" minor_version=\"1\" />";
const OAR_DIRECTORIES: [&str; 5] = ["assets", "objects", "terrains", "landdata", "settings"];

type AsyncCallback = Box<dyn Fn(&dyn std::any::Any) + Send + Sync>;
type TerrainCallback = Box<dyn Fn(Vec<Vec<f32>>, i64, i64) + Send + Sync>;

fn archive_error(context: &'static str) -> Error {
    Error::Parse {
        position: 0,
        context,
    }
}

fn io_error(_: std::io::Error) -> Error {
    Error::InvalidOperation
}

fn validate_archive_path(path: &str) -> Result<(), Error> {
    if path.is_empty()
        || path.len() > MAX_PATH_BYTES
        || path.contains('\0')
        || path.contains('\\')
        || path.starts_with('/')
    {
        return Err(archive_error("unsafe archive path"));
    }
    let mut depth = 0usize;
    for component in Path::new(path).components() {
        match component {
            Component::Normal(_) => depth += 1,
            Component::CurDir => {}
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
                return Err(archive_error("archive path traversal"));
            }
        }
    }
    if depth == 0 || depth > MAX_PATH_DEPTH {
        return Err(archive_error("archive path depth limit"));
    }
    Ok(())
}

fn octal(bytes: &[u8]) -> Result<u64, Error> {
    let text = std::str::from_utf8(bytes)
        .map_err(|_| archive_error("tar octal UTF-8"))?
        .trim_matches(['\0', ' ']);
    if text.is_empty() {
        return Ok(0);
    }
    if !text.bytes().all(|byte| matches!(byte, b'0'..=b'7')) {
        return Err(archive_error("tar octal field"));
    }
    u64::from_str_radix(text, 8).map_err(|_| archive_error("tar octal overflow"))
}

fn padded_octal(value: u64, width: usize) -> Result<Vec<u8>, Error> {
    if width < 2 {
        return Err(Error::Argument);
    }
    let digits = format!("{value:o}");
    if digits.len() + 1 > width {
        return Err(Error::Argument);
    }
    let mut result = vec![b'0'; width];
    let start = width - 1 - digits.len();
    result[start..width - 1].copy_from_slice(digits.as_bytes());
    result[width - 1] = 0;
    Ok(result)
}

fn split_ustar_path(path: &str) -> Result<(&str, &str), Error> {
    validate_archive_path(path)?;
    if path.len() <= 100 {
        return Ok(("", path));
    }
    for (index, _) in path.match_indices('/').rev() {
        let prefix = &path[..index];
        let name = &path[index + 1..];
        if prefix.len() <= 155 && name.len() <= 100 && !name.is_empty() {
            return Ok((prefix, name));
        }
    }
    Err(archive_error("tar path exceeds ustar limits"))
}

fn write_header(
    writer: &mut dyn Write,
    path: &str,
    size: usize,
    entry_type: u8,
) -> Result<(), Error> {
    if size > MAX_ENTRY_BYTES {
        return Err(archive_error("tar entry size limit"));
    }
    let (prefix, name) = split_ustar_path(path)?;
    let mut header = [0u8; BLOCK];
    header[..name.len()].copy_from_slice(name.as_bytes());
    header[100..108].copy_from_slice(&padded_octal(
        if entry_type == b'5' { 0o755 } else { 0o644 },
        8,
    )?);
    header[108..116].copy_from_slice(&padded_octal(0, 8)?);
    header[116..124].copy_from_slice(&padded_octal(0, 8)?);
    header[124..136].copy_from_slice(&padded_octal(size as u64, 12)?);
    header[136..148].copy_from_slice(&padded_octal(0, 12)?);
    header[148..156].fill(b' ');
    header[156] = entry_type;
    header[257..263].copy_from_slice(b"ustar\0");
    header[263..265].copy_from_slice(b"00");
    header[345..345 + prefix.len()].copy_from_slice(prefix.as_bytes());
    let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
    header[148..156].copy_from_slice(&padded_octal(checksum, 8)?);
    writer.write_all(&header).map_err(io_error)
}

fn write_entry(
    writer: &mut dyn Write,
    path: &str,
    data: &[u8],
    entry_type: u8,
) -> Result<(), Error> {
    write_header(writer, path, data.len(), entry_type)?;
    writer.write_all(data).map_err(io_error)?;
    let padding = (BLOCK - data.len() % BLOCK) % BLOCK;
    if padding != 0 {
        writer.write_all(&vec![0; padding]).map_err(io_error)?;
    }
    Ok(())
}

struct ReaderState {
    stream: Box<dyn ReadWrite + Send>,
    entries: usize,
    total_bytes: usize,
    finished: bool,
}

pub struct TarArchiveReader {
    state: Mutex<ReaderState>,
}

impl std::fmt::Debug for TarArchiveReader {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("TarArchiveReader")
            .finish_non_exhaustive()
    }
}

impl TarArchiveReader {
    pub fn new(stream: Box<dyn ReadWrite + Send>) -> Result<Self, Error> {
        Ok(Self {
            state: Mutex::new(ReaderState {
                stream,
                entries: 0,
                total_bytes: 0,
                finished: false,
            }),
        })
    }

    pub fn close(&self) -> Result<(), Error> {
        self.state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .finished = true;
        Ok(())
    }

    pub fn convert_octal_bytes_to_decimal(
        bytes: Vec<u8>,
        start_index: i32,
        count: i32,
    ) -> Result<i32, Error> {
        let start = usize::try_from(start_index).map_err(|_| Error::IndexOutOfRange)?;
        let count = usize::try_from(count).map_err(|_| Error::IndexOutOfRange)?;
        let end = start.checked_add(count).ok_or(Error::IndexOutOfRange)?;
        let value = octal(bytes.get(start..end).ok_or(Error::IndexOutOfRange)?)?;
        i32::try_from(value).map_err(|_| Error::IndexOutOfRange)
    }

    pub fn read_entry(
        &self,
        file_path: &mut String,
        entry_type: &mut TarArchiveReaderTarEntryType,
    ) -> Result<Option<Vec<u8>>, Error> {
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if state.finished {
            return Ok(None);
        }
        let mut header = [0u8; BLOCK];
        state.stream.read_exact(&mut header).map_err(io_error)?;
        if header.iter().all(|byte| *byte == 0) {
            state.finished = true;
            return Ok(None);
        }
        let recorded = octal(&header[148..156])?;
        let mut checksum_header = header;
        checksum_header[148..156].fill(b' ');
        let computed: u64 = checksum_header.iter().map(|byte| u64::from(*byte)).sum();
        if recorded != computed {
            return Err(archive_error("tar checksum"));
        }
        let name = nul_string(&header[..100])?;
        let prefix = nul_string(&header[345..500])?;
        let path = if prefix.is_empty() {
            name
        } else {
            format!("{prefix}/{name}")
        };
        validate_archive_path(&path)?;
        let size = usize::try_from(octal(&header[124..136])?)
            .map_err(|_| archive_error("tar size overflow"))?;
        if size > MAX_ENTRY_BYTES {
            return Err(archive_error("tar entry size limit"));
        }
        state.entries = state
            .entries
            .checked_add(1)
            .ok_or_else(|| archive_error("tar entry count overflow"))?;
        state.total_bytes = state
            .total_bytes
            .checked_add(size)
            .ok_or_else(|| archive_error("tar total size overflow"))?;
        if state.entries > MAX_ENTRIES || state.total_bytes > MAX_TOTAL_BYTES {
            return Err(archive_error("tar archive limits"));
        }
        let kind = match header[156] {
            0 | b'0' => TarArchiveReaderTarEntryType::TYPE_NORMAL_FILE,
            b'1' => TarArchiveReaderTarEntryType::TYPE_HARD_LINK,
            b'2' => TarArchiveReaderTarEntryType::TYPE_SYMBOLIC_LINK,
            b'3' => TarArchiveReaderTarEntryType::TYPE_CHAR_SPECIAL,
            b'4' => TarArchiveReaderTarEntryType::TYPE_BLOCK_SPECIAL,
            b'5' => TarArchiveReaderTarEntryType::TYPE_DIRECTORY,
            b'6' => TarArchiveReaderTarEntryType::TYPE_FIFO,
            b'7' => TarArchiveReaderTarEntryType::TYPE_CONTIGUOUS_FILE,
            _ => TarArchiveReaderTarEntryType::TYPE_UNKNOWN,
        };
        if matches!(
            kind,
            TarArchiveReaderTarEntryType::TYPE_SYMBOLIC_LINK
                | TarArchiveReaderTarEntryType::TYPE_HARD_LINK
        ) {
            return Err(archive_error("tar links are not permitted"));
        }
        let mut data = vec![0u8; size];
        state.stream.read_exact(&mut data).map_err(io_error)?;
        let padding = (BLOCK - size % BLOCK) % BLOCK;
        if padding != 0 {
            state
                .stream
                .seek(SeekFrom::Current(padding as i64))
                .map_err(io_error)?;
        }
        *file_path = path;
        *entry_type = kind;
        Ok(Some(data))
    }
}

fn nul_string(bytes: &[u8]) -> Result<String, Error> {
    let end = bytes
        .iter()
        .position(|byte| *byte == 0)
        .unwrap_or(bytes.len());
    std::str::from_utf8(&bytes[..end])
        .map(str::to_owned)
        .map_err(|_| archive_error("tar path UTF-8"))
}

struct WriterState {
    stream: Box<dyn ReadWrite + Send>,
    entries: usize,
    total_bytes: usize,
    closed: bool,
}

pub struct TarArchiveWriter {
    state: Mutex<WriterState>,
}

#[derive(Clone)]
pub struct OarFileAssetLoadedCallback(
    std::sync::Arc<dyn Fn(crate::assets::Asset, i64, i64) + Send + Sync>,
);
impl OarFileAssetLoadedCallback {
    pub fn from_callback(
        callback: impl Fn(crate::assets::Asset, i64, i64) + Send + Sync + 'static,
    ) -> Self {
        Self(std::sync::Arc::new(callback))
    }
    pub fn new(
        _object: libremetaverse_types::compat::Object,
        _method: isize,
    ) -> Result<Self, Error> {
        Err(Error::Argument)
    }
    pub fn invoke(
        &self,
        asset: crate::assets::Asset,
        bytes_read: i64,
        total_bytes: i64,
    ) -> Result<(), Error> {
        (self.0)(asset, bytes_read, total_bytes);
        Ok(())
    }
    pub fn begin_invoke(
        &self,
        asset: crate::assets::Asset,
        bytes_read: i64,
        total_bytes: i64,
        callback: AsyncCallback,
        _object: libremetaverse_types::compat::Object,
    ) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
        self.invoke(asset, bytes_read, total_bytes)?;
        callback(&());
        Ok(Box::new(()))
    }
    pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
        Ok(())
    }
}

#[derive(Clone)]
pub struct OarFileTerrainLoadedCallback(std::sync::Arc<dyn Fn(Vec<f32>, i64, i64) + Send + Sync>);
impl OarFileTerrainLoadedCallback {
    pub fn from_callback(callback: impl Fn(Vec<f32>, i64, i64) + Send + Sync + 'static) -> Self {
        Self(std::sync::Arc::new(callback))
    }
    pub fn new(
        _object: libremetaverse_types::compat::Object,
        _method: isize,
    ) -> Result<Self, Error> {
        Err(Error::Argument)
    }
    pub fn invoke(
        &self,
        terrain: Vec<f32>,
        bytes_read: i64,
        total_bytes: i64,
    ) -> Result<(), Error> {
        (self.0)(terrain, bytes_read, total_bytes);
        Ok(())
    }
    pub fn begin_invoke(
        &self,
        terrain: Vec<f32>,
        bytes_read: i64,
        total_bytes: i64,
        callback: AsyncCallback,
        _object: libremetaverse_types::compat::Object,
    ) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
        self.invoke(terrain, bytes_read, total_bytes)?;
        callback(&());
        Ok(Box::new(()))
    }
    pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
        Ok(())
    }
}

#[derive(Clone)]
pub struct OarFileSettingsLoadedCallback(
    std::sync::Arc<dyn Fn(String, RegionSettings) + Send + Sync>,
);
impl OarFileSettingsLoadedCallback {
    pub fn from_callback(
        callback: impl Fn(String, RegionSettings) + Send + Sync + 'static,
    ) -> Self {
        Self(std::sync::Arc::new(callback))
    }
    pub fn new(
        _object: libremetaverse_types::compat::Object,
        _method: isize,
    ) -> Result<Self, Error> {
        Err(Error::Argument)
    }
    pub fn invoke(&self, region_name: String, settings: RegionSettings) -> Result<(), Error> {
        (self.0)(region_name, settings);
        Ok(())
    }
    pub fn begin_invoke(
        &self,
        region_name: String,
        settings: RegionSettings,
        callback: AsyncCallback,
        _object: libremetaverse_types::compat::Object,
    ) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
        self.invoke(region_name, settings)?;
        callback(&());
        Ok(Box::new(()))
    }
    pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
        Ok(())
    }
}

#[derive(Clone)]
pub struct OarFileSceneObjectLoadedCallback(
    std::sync::Arc<dyn Fn(crate::assets::AssetPrim, i64, i64) + Send + Sync>,
);
impl OarFileSceneObjectLoadedCallback {
    pub fn from_callback(
        callback: impl Fn(crate::assets::AssetPrim, i64, i64) + Send + Sync + 'static,
    ) -> Self {
        Self(std::sync::Arc::new(callback))
    }
    pub fn new(
        _object: libremetaverse_types::compat::Object,
        _method: isize,
    ) -> Result<Self, Error> {
        Err(Error::Argument)
    }
    pub fn invoke(
        &self,
        linkset: crate::assets::AssetPrim,
        bytes_read: i64,
        total_bytes: i64,
    ) -> Result<(), Error> {
        (self.0)(linkset, bytes_read, total_bytes);
        Ok(())
    }
    pub fn begin_invoke(
        &self,
        linkset: crate::assets::AssetPrim,
        bytes_read: i64,
        total_bytes: i64,
        callback: AsyncCallback,
        _object: libremetaverse_types::compat::Object,
    ) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
        self.invoke(linkset, bytes_read, total_bytes)?;
        callback(&());
        Ok(Box::new(()))
    }
    pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
        Ok(())
    }
}

#[derive(Clone, Debug)]
pub struct RegionSettings {
    pub agent_limit: i32,
    pub allow_damage: bool,
    pub allow_land_join_divide: bool,
    pub allow_land_resell: bool,
    pub block_fly: bool,
    pub block_land_show_in_search: bool,
    pub block_terraform: bool,
    pub disable_collisions: bool,
    pub disable_physics: bool,
    pub disable_scripts: bool,
    pub fixed_sun: bool,
    pub maturity_rating: i32,
    pub object_bonus: f32,
    pub restrict_pushing: bool,
    pub terrain_detail0: UUID,
    pub terrain_detail1: UUID,
    pub terrain_detail2: UUID,
    pub terrain_detail3: UUID,
    pub terrain_height_range00: f32,
    pub terrain_height_range01: f32,
    pub terrain_height_range10: f32,
    pub terrain_height_range11: f32,
    pub terrain_lower_limit: f32,
    pub terrain_raise_limit: f32,
    pub terrain_start_height00: f32,
    pub terrain_start_height01: f32,
    pub terrain_start_height10: f32,
    pub terrain_start_height11: f32,
    pub use_estate_sun: bool,
    pub water_height: f32,
}
impl RegionSettings {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }
    pub fn from_stream(mut stream: Box<dyn ReadWrite + Send>) -> Result<Self, Error> {
        stream.seek(SeekFrom::Start(0)).map_err(io_error)?;
        let mut bytes = Vec::new();
        stream
            .take((MAX_ENTRY_BYTES + 1) as u64)
            .read_to_end(&mut bytes)
            .map_err(io_error)?;
        if bytes.len() > MAX_ENTRY_BYTES {
            return Err(Error::Argument);
        }
        let text =
            std::str::from_utf8(&bytes).map_err(|_| archive_error("region settings UTF-8"))?;
        let document =
            roxmltree::Document::parse(text).map_err(|_| archive_error("region settings XML"))?;
        if !document.root_element().has_tag_name("RegionSettings") {
            return Err(archive_error("region settings root"));
        }
        let mut settings = Self::default();
        for node in document.descendants().filter(|node| node.is_element()) {
            let text = node.text().unwrap_or_default().trim();
            match node.tag_name().name() {
                "AllowDamage" => settings.allow_damage = parse_bool(text)?,
                "AllowLandResell" => settings.allow_land_resell = parse_bool(text)?,
                "AllowLandJoinDivide" => settings.allow_land_join_divide = parse_bool(text)?,
                "BlockFly" => settings.block_fly = parse_bool(text)?,
                "BlockLandShowInSearch" => settings.block_land_show_in_search = parse_bool(text)?,
                "BlockTerraform" => settings.block_terraform = parse_bool(text)?,
                "DisableCollisions" => settings.disable_collisions = parse_bool(text)?,
                "DisablePhysics" => settings.disable_physics = parse_bool(text)?,
                "DisableScripts" => settings.disable_scripts = parse_bool(text)?,
                "FixedSun" => settings.fixed_sun = parse_bool(text)?,
                "RestrictPushing" => settings.restrict_pushing = parse_bool(text)?,
                "UseEstateSun" => settings.use_estate_sun = parse_bool(text)?,
                "MaturityRating" => settings.maturity_rating = parse_i32(text)?,
                "AgentLimit" => settings.agent_limit = parse_i32(text)?,
                "ObjectBonus" => settings.object_bonus = parse_f32(text)?,
                "WaterHeight" => settings.water_height = parse_f32(text)?,
                "TerrainRaiseLimit" => settings.terrain_raise_limit = parse_f32(text)?,
                "TerrainLowerLimit" => settings.terrain_lower_limit = parse_f32(text)?,
                "Texture1" => settings.terrain_detail0 = UUID::new_with_string(text.to_owned())?,
                "Texture2" => settings.terrain_detail1 = UUID::new_with_string(text.to_owned())?,
                "Texture3" => settings.terrain_detail2 = UUID::new_with_string(text.to_owned())?,
                "Texture4" => settings.terrain_detail3 = UUID::new_with_string(text.to_owned())?,
                "ElevationLowSW" => settings.terrain_start_height00 = parse_f32(text)?,
                "ElevationLowNW" => settings.terrain_start_height01 = parse_f32(text)?,
                "ElevationLowSE" => settings.terrain_start_height10 = parse_f32(text)?,
                "ElevationLowNE" => settings.terrain_start_height11 = parse_f32(text)?,
                "ElevationHighSW" => settings.terrain_height_range00 = parse_f32(text)?,
                "ElevationHighNW" => settings.terrain_height_range01 = parse_f32(text)?,
                "ElevationHighSE" => settings.terrain_height_range10 = parse_f32(text)?,
                "ElevationHighNE" => settings.terrain_height_range11 = parse_f32(text)?,
                _ => {}
            }
        }
        Ok(settings)
    }
    pub fn to_xml(&self, filename: String) -> Result<(), Error> {
        fs::write(filename, self.xml()).map_err(io_error)
    }
    fn xml(&self) -> String {
        format!(
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RegionSettings><General><AllowDamage>{}</AllowDamage><AllowLandResell>{}</AllowLandResell><AllowLandJoinDivide>{}</AllowLandJoinDivide><BlockFly>{}</BlockFly><BlockLandShowInSearch>{}</BlockLandShowInSearch><BlockTerraform>{}</BlockTerraform><DisableCollisions>{}</DisableCollisions><DisablePhysics>{}</DisablePhysics><DisableScripts>{}</DisableScripts><MaturityRating>{}</MaturityRating><RestrictPushing>{}</RestrictPushing><AgentLimit>{}</AgentLimit><ObjectBonus>{}</ObjectBonus></General><GroundTextures><Texture1>{}</Texture1><Texture2>{}</Texture2><Texture3>{}</Texture3><Texture4>{}</Texture4><ElevationLowSW>{}</ElevationLowSW><ElevationLowNW>{}</ElevationLowNW><ElevationLowSE>{}</ElevationLowSE><ElevationLowNE>{}</ElevationLowNE><ElevationHighSW>{}</ElevationHighSW><ElevationHighNW>{}</ElevationHighNW><ElevationHighSE>{}</ElevationHighSE><ElevationHighNE>{}</ElevationHighNE></GroundTextures><Terrain><WaterHeight>{}</WaterHeight><TerrainRaiseLimit>{}</TerrainRaiseLimit><TerrainLowerLimit>{}</TerrainLowerLimit><UseEstateSun>{}</UseEstateSun><FixedSun>{}</FixedSun></Terrain></RegionSettings>\n",
            self.allow_damage,
            self.allow_land_resell,
            self.allow_land_join_divide,
            self.block_fly,
            self.block_land_show_in_search,
            self.block_terraform,
            self.disable_collisions,
            self.disable_physics,
            self.disable_scripts,
            self.maturity_rating,
            self.restrict_pushing,
            self.agent_limit,
            self.object_bonus,
            self.terrain_detail0.to_string(),
            self.terrain_detail1.to_string(),
            self.terrain_detail2.to_string(),
            self.terrain_detail3.to_string(),
            self.terrain_start_height00,
            self.terrain_start_height01,
            self.terrain_start_height10,
            self.terrain_start_height11,
            self.terrain_height_range00,
            self.terrain_height_range01,
            self.terrain_height_range10,
            self.terrain_height_range11,
            self.water_height,
            self.terrain_raise_limit,
            self.terrain_lower_limit,
            self.use_estate_sun,
            self.fixed_sun
        )
    }
}
impl Default for RegionSettings {
    fn default() -> Self {
        Self {
            agent_limit: 0,
            allow_damage: false,
            allow_land_join_divide: false,
            allow_land_resell: false,
            block_fly: false,
            block_land_show_in_search: false,
            block_terraform: false,
            disable_collisions: false,
            disable_physics: false,
            disable_scripts: false,
            fixed_sun: false,
            maturity_rating: 0,
            object_bonus: 0.0,
            restrict_pushing: false,
            terrain_detail0: UUID::zero(),
            terrain_detail1: UUID::zero(),
            terrain_detail2: UUID::zero(),
            terrain_detail3: UUID::zero(),
            terrain_height_range00: 0.0,
            terrain_height_range01: 0.0,
            terrain_height_range10: 0.0,
            terrain_height_range11: 0.0,
            terrain_lower_limit: 0.0,
            terrain_raise_limit: 0.0,
            terrain_start_height00: 0.0,
            terrain_start_height01: 0.0,
            terrain_start_height10: 0.0,
            terrain_start_height11: 0.0,
            use_estate_sun: false,
            water_height: 0.0,
        }
    }
}
fn parse_bool(value: &str) -> Result<bool, Error> {
    match value.to_ascii_lowercase().as_str() {
        "true" => Ok(true),
        "false" => Ok(false),
        _ => Err(archive_error("region settings boolean")),
    }
}
fn parse_i32(value: &str) -> Result<i32, Error> {
    value
        .parse()
        .map_err(|_| archive_error("region settings integer"))
}
fn parse_f32(value: &str) -> Result<f32, Error> {
    value
        .parse::<f32>()
        .ok()
        .filter(|value| value.is_finite())
        .ok_or_else(|| archive_error("region settings number"))
}
impl std::fmt::Debug for TarArchiveWriter {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("TarArchiveWriter")
            .finish_non_exhaustive()
    }
}
impl TarArchiveWriter {
    pub fn new(stream: Box<dyn ReadWrite + Send>) -> Result<Self, Error> {
        Ok(Self {
            state: Mutex::new(WriterState {
                stream,
                entries: 0,
                total_bytes: 0,
                closed: false,
            }),
        })
    }
    pub fn convert_decimal_to_padded_octal_bytes(
        value: i32,
        padding: i32,
    ) -> Result<Vec<u8>, Error> {
        padded_octal(
            u64::try_from(value).map_err(|_| Error::Argument)?,
            usize::try_from(padding).map_err(|_| Error::Argument)?,
        )
    }
    pub fn write_dir(&self, dir_name: String) -> Result<(), Error> {
        self.write(&dir_name, &[], b'5')
    }
    pub fn write_file_with_string_bytes(
        &self,
        file_path: String,
        data: Vec<u8>,
    ) -> Result<(), Error> {
        self.write(&file_path, &data, b'0')
    }
    pub fn write_file_with_string_string(
        &self,
        file_path: String,
        data: String,
    ) -> Result<(), Error> {
        self.write(&file_path, data.as_bytes(), b'0')
    }
    fn write(&self, path: &str, data: &[u8], kind: u8) -> Result<(), Error> {
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if state.closed {
            return Err(Error::InvalidOperation);
        }
        state.entries = state
            .entries
            .checked_add(1)
            .ok_or(Error::InvalidOperation)?;
        state.total_bytes = state
            .total_bytes
            .checked_add(data.len())
            .ok_or(Error::InvalidOperation)?;
        if state.entries > MAX_ENTRIES || state.total_bytes > MAX_TOTAL_BYTES {
            return Err(archive_error("tar archive limits"));
        }
        write_entry(&mut state.stream, path, data, kind)
    }
    pub fn close(&self) -> Result<(), Error> {
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        if !state.closed {
            state
                .stream
                .write_all(&[0u8; BLOCK * 2])
                .map_err(io_error)?;
            state.stream.flush().map_err(io_error)?;
            state.closed = true;
        }
        Ok(())
    }
}

pub(crate) fn clear_asset_folder(assets_path: String) -> Result<(), Error> {
    let root = PathBuf::from(assets_path);
    if !root.is_dir() {
        return fs::create_dir_all(root).map_err(io_error);
    }
    for entry in fs::read_dir(&root).map_err(io_error)? {
        let entry = entry.map_err(io_error)?;
        let metadata = fs::symlink_metadata(entry.path()).map_err(io_error)?;
        if metadata.file_type().is_symlink() || metadata.is_file() {
            fs::remove_file(entry.path()).map_err(io_error)?;
        } else if metadata.is_dir() {
            fs::remove_dir_all(entry.path()).map_err(io_error)?;
        }
    }
    Ok(())
}

fn asset_extension(asset_type: AssetType) -> Option<&'static str> {
    match asset_type {
        AssetType::Animation => Some("_animation.bvh"),
        AssetType::Bodypart => Some("_bodypart.txt"),
        AssetType::CallingCard => Some("_callingcard.txt"),
        AssetType::Clothing => Some("_clothing.txt"),
        AssetType::Folder => Some("_folder.txt"),
        AssetType::Gesture => Some("_gesture.txt"),
        AssetType::ImageJPEG => Some("_image.jpg"),
        AssetType::ImageTGA => Some("_image.tga"),
        AssetType::Landmark => Some("_landmark.txt"),
        AssetType::LSLBytecode => Some("_bytecode.lso"),
        AssetType::LSLText => Some("_script.lsl"),
        AssetType::Notecard => Some("_notecard.txt"),
        AssetType::Object => Some("_object.xml"),
        AssetType::Simstate => Some("_simstate.bin"),
        AssetType::Sound => Some("_sound.ogg"),
        AssetType::SoundWAV => Some("_sound.wav"),
        AssetType::Texture => Some("_texture.jp2"),
        AssetType::TextureTGA => Some("_texture.tga"),
        AssetType::Mesh => Some("_mesh.llmesh"),
        AssetType::Material => Some("_material.llsd"),
        AssetType::Settings => Some("_settings.llsd"),
        _ => None,
    }
}

pub(crate) fn asset_type_to_extension()
-> libremetaverse_types::compat::FrozenDictionary<AssetType, String> {
    let types = [
        AssetType::Animation,
        AssetType::Bodypart,
        AssetType::CallingCard,
        AssetType::Clothing,
        AssetType::Folder,
        AssetType::Gesture,
        AssetType::ImageJPEG,
        AssetType::ImageTGA,
        AssetType::Landmark,
        AssetType::LSLBytecode,
        AssetType::LSLText,
        AssetType::Notecard,
        AssetType::Object,
        AssetType::Simstate,
        AssetType::Sound,
        AssetType::SoundWAV,
        AssetType::Texture,
        AssetType::TextureTGA,
        AssetType::Mesh,
        AssetType::Material,
        AssetType::Settings,
    ];
    libremetaverse_types::compat::FrozenDictionary(
        types
            .into_iter()
            .filter_map(|kind| asset_extension(kind).map(|suffix| (kind, suffix.to_owned())))
            .collect(),
    )
}

pub(crate) fn extension_to_asset_type()
-> libremetaverse_types::compat::FrozenDictionary<String, AssetType> {
    libremetaverse_types::compat::FrozenDictionary(
        asset_type_to_extension()
            .0
            .into_iter()
            .map(|(kind, suffix)| (suffix, kind))
            .collect(),
    )
}

fn prepare_output_directory(path: &str) -> Result<PathBuf, Error> {
    let root = PathBuf::from(path);
    if root.exists() {
        let metadata = fs::symlink_metadata(&root).map_err(io_error)?;
        if metadata.file_type().is_symlink() || !metadata.is_dir() {
            return Err(archive_error("archive output root"));
        }
    } else {
        fs::create_dir_all(&root).map_err(io_error)?;
    }
    Ok(root)
}

fn write_asset(root: &Path, id: UUID, extension: &str, data: &[u8]) -> Result<(), Error> {
    if data.len() > MAX_ENTRY_BYTES {
        return Err(archive_error("asset size limit"));
    }
    let name = format!("{}{extension}", id);
    validate_archive_path(&name)?;
    fs::write(root.join(name), data).map_err(io_error)
}

pub(crate) async fn save_assets(
    asset_manager: crate::AssetManager,
    asset_type: AssetType,
    mut assets: Vec<UUID>,
    assets_path: String,
) -> Result<(), Error> {
    if assets.len() > MAX_ENTRIES {
        return Err(archive_error("asset count limit"));
    }
    let extension = asset_extension(asset_type).ok_or(Error::Argument)?;
    let root = prepare_output_directory(&assets_path)?;
    assets.sort_by_key(UUID::to_string);
    assets.dedup();
    let mut total = 0usize;
    for id in assets {
        if id == UUID::zero() {
            continue;
        }
        let bytes = if asset_type == AssetType::Texture {
            asset_manager
                .request_image(id, None, None)
                .await?
                .map(|asset| asset.native_bytes())
        } else {
            asset_manager
                .request_asset_with_uuid_asset_type_boolean_cancellation_token(
                    id, asset_type, false, None,
                )
                .await?
                .map(|asset| asset.asset_data)
        };
        if let Some(bytes) = bytes {
            total = total
                .checked_add(bytes.len())
                .ok_or_else(|| archive_error("asset total size overflow"))?;
            if total > MAX_TOTAL_BYTES {
                return Err(archive_error("asset total size limit"));
            }
            write_asset(&root, id, extension, &bytes)?;
        }
    }
    Ok(())
}

pub(crate) async fn save_sim_asset(
    asset_manager: crate::AssetManager,
    asset_type: AssetType,
    asset_id: UUID,
    item_id: UUID,
    prim_id: UUID,
    assets_path: String,
) -> Result<(), Error> {
    let extension = asset_extension(asset_type).ok_or(Error::Argument)?;
    let root = prepare_output_directory(&assets_path)?;
    let asset = asset_manager
        .request_asset_with_uuid_uuid_uuid_asset_type_boolean_source_type_uuid_cancellation_token(
            asset_id,
            item_id,
            prim_id,
            asset_type,
            false,
            crate::SourceType::SimInventoryItem,
            UUID::random()?,
            None,
        )
        .await?;
    if let Some(asset) = asset {
        write_asset(&root, asset_id, extension, &asset.asset_data)?;
    }
    Ok(())
}

pub(crate) fn package_archive(directory_name: String, filename: String) -> Result<(), Error> {
    let root = fs::canonicalize(&directory_name).map_err(io_error)?;
    if !root.is_dir() {
        return Err(Error::Argument);
    }
    let output = File::create(filename).map_err(io_error)?;
    let mut gzip = GzBuilder::new()
        .mtime(0)
        .write(output, Compression::default());
    write_entry(&mut gzip, "archive.xml", ARCHIVE_XML.as_bytes(), b'0')?;
    let mut entries = Vec::new();
    for directory in OAR_DIRECTORIES {
        let path = root.join(directory);
        if path.is_dir() {
            collect_files(&root, &path, 1, &mut entries)?;
        }
    }
    entries.sort_by(|left, right| left.0.cmp(&right.0));
    let mut total = ARCHIVE_XML.len();
    if entries.len() + 1 > MAX_ENTRIES {
        return Err(archive_error("OAR entry count limit"));
    }
    for (archive_path, disk_path) in entries {
        let data = fs::read(disk_path).map_err(io_error)?;
        total = total
            .checked_add(data.len())
            .ok_or_else(|| archive_error("OAR total size overflow"))?;
        if data.len() > MAX_ENTRY_BYTES || total > MAX_TOTAL_BYTES {
            return Err(archive_error("OAR size limit"));
        }
        write_entry(&mut gzip, &archive_path, &data, b'0')?;
    }
    gzip.write_all(&[0u8; BLOCK * 2]).map_err(io_error)?;
    gzip.finish().map_err(io_error)?;
    Ok(())
}

fn collect_files(
    root: &Path,
    directory: &Path,
    depth: usize,
    result: &mut Vec<(String, PathBuf)>,
) -> Result<(), Error> {
    if depth > MAX_PATH_DEPTH {
        return Err(archive_error("OAR directory depth limit"));
    }
    let mut children = fs::read_dir(directory)
        .map_err(io_error)?
        .collect::<Result<Vec<_>, _>>()
        .map_err(io_error)?;
    children.sort_by_key(std::fs::DirEntry::file_name);
    for child in children {
        let metadata = fs::symlink_metadata(child.path()).map_err(io_error)?;
        if metadata.file_type().is_symlink() {
            return Err(archive_error("OAR symlink rejected"));
        }
        if metadata.is_dir() {
            collect_files(root, &child.path(), depth + 1, result)?;
        } else if metadata.is_file() {
            let relative = child
                .path()
                .strip_prefix(root)
                .map_err(|_| archive_error("OAR extraction root"))?
                .to_string_lossy()
                .replace('\\', "/");
            validate_archive_path(&relative)?;
            result.push((relative, child.path()));
        }
    }
    Ok(())
}

pub(crate) fn unpackage_archive(
    filename: String,
    asset_callback: OarFileAssetLoadedCallback,
    terrain_callback: OarFileTerrainLoadedCallback,
    _object_callback: OarFileSceneObjectLoadedCallback,
    settings_callback: OarFileSettingsLoadedCallback,
) -> Result<(), Error> {
    let file = File::open(filename).map_err(io_error)?;
    let total = i64::try_from(file.metadata().map_err(io_error)?.len())
        .map_err(|_| Error::InvalidOperation)?;
    let decoder = GzDecoder::new(file);
    let mut archive = tar::Archive::new(decoder);
    let mut count = 0usize;
    let mut expanded = 0usize;
    for item in archive.entries().map_err(io_error)? {
        let entry = item.map_err(io_error)?;
        count += 1;
        if count > MAX_ENTRIES {
            return Err(archive_error("OAR entry count limit"));
        }
        let path = entry
            .path()
            .map_err(io_error)?
            .to_string_lossy()
            .replace('\\', "/");
        validate_archive_path(&path)?;
        let size = usize::try_from(entry.size()).map_err(|_| archive_error("OAR entry size"))?;
        expanded = expanded
            .checked_add(size)
            .ok_or_else(|| archive_error("OAR expanded size overflow"))?;
        if size > MAX_ENTRY_BYTES || expanded > MAX_TOTAL_BYTES {
            return Err(archive_error("OAR expanded size limit"));
        }
        if entry.header().entry_type().is_symlink() || entry.header().entry_type().is_hard_link() {
            return Err(archive_error("OAR links are not permitted"));
        }
        if !entry.header().entry_type().is_file() {
            continue;
        }
        let mut data = Vec::with_capacity(size);
        entry
            .take((MAX_ENTRY_BYTES + 1) as u64)
            .read_to_end(&mut data)
            .map_err(io_error)?;
        if data.len() != size {
            return Err(archive_error("OAR entry length"));
        }
        if path.starts_with("terrains/") {
            let flattened = decode_terrain(&path, &data)?.unwrap_or_default();
            if !flattened.is_empty() {
                terrain_callback.invoke(flattened, expanded as i64, total)?;
            }
        } else if path.starts_with("assets/") {
            if let Some(asset) = load_generic_asset(&path, data)? {
                asset_callback.invoke(asset, expanded as i64, total)?;
            }
        } else if path.starts_with("settings/") && path.ends_with(".xml") {
            let settings = RegionSettings::from_stream(Box::new(std::io::Cursor::new(data)))?;
            let name = Path::new(&path)
                .file_stem()
                .and_then(|value| value.to_str())
                .ok_or_else(|| archive_error("OAR settings name"))?
                .to_owned();
            settings_callback.invoke(name, settings)?;
        }
    }
    Ok(())
}

fn load_generic_asset(path: &str, data: Vec<u8>) -> Result<Option<crate::assets::Asset>, Error> {
    let filename = path
        .rsplit('/')
        .next()
        .ok_or_else(|| archive_error("OAR asset name"))?;
    let Some((suffix, asset_type)) = extension_asset_type(filename) else {
        return Ok(None);
    };
    let Some(id_text) = filename.strip_suffix(suffix) else {
        return Ok(None);
    };
    let id = UUID::new_with_string(id_text.to_owned())?;
    crate::asset_models::Asset::native_new(asset_type, id, data).map(Some)
}

fn extension_asset_type(filename: &str) -> Option<(&'static str, AssetType)> {
    asset_type_to_extension()
        .0
        .into_iter()
        .find_map(|(kind, _)| {
            asset_extension(kind)
                .filter(|suffix| filename.ends_with(suffix))
                .map(|suffix| (suffix, kind))
        })
}

fn decode_terrain(path: &str, data: &[u8]) -> Result<Option<Vec<f32>>, Error> {
    if !path.ends_with(".r32") && !path.ends_with(".f32") {
        return Ok(None);
    }
    if data.is_empty() || !data.len().is_multiple_of(4) {
        return Ok(None);
    }
    let posts = data.len() / 4;
    let side = (posts as f64).sqrt() as usize;
    if side == 0 || side.checked_mul(side) != Some(posts) {
        return Ok(None);
    }
    let mut terrain = Vec::with_capacity(posts);
    for bytes in data.chunks_exact(4) {
        let value = f32::from_le_bytes(
            bytes
                .try_into()
                .map_err(|_| archive_error("terrain float"))?,
        );
        if !value.is_finite() {
            return Err(archive_error("terrain non-finite height"));
        }
        terrain.push(value.clamp(0.0, 255.0));
    }
    Ok(Some(terrain))
}

pub(crate) fn load_terrain(
    path: &str,
    data: &[u8],
    callback: TerrainCallback,
    bytes_read: i64,
    total_bytes: i64,
) -> Result<bool, Error> {
    let Some(flat) = decode_terrain(path, data)? else {
        return Ok(false);
    };
    let side = (flat.len() as f64).sqrt() as usize;
    let rows = flat.chunks_exact(side).map(<[f32]>::to_vec).collect();
    callback(rows, bytes_read, total_bytes);
    Ok(true)
}

pub(crate) fn save_terrain(sim: crate::Simulator, terrain_path: String) -> Result<(), Error> {
    let root = PathBuf::from(terrain_path);
    if root.exists() {
        fs::remove_dir_all(&root).map_err(io_error)?;
    }
    fs::create_dir_all(&root).map_err(io_error)?;
    let name = sim.name.clone();
    let safe_name = name.replace(['/', '\\'], "_");
    let mut file = File::create(root.join(format!("{safe_name}.r32"))).map_err(io_error)?;
    for y in 0..sim.size_y {
        for x in 0..sim.size_x {
            let mut height = 0.0;
            if !sim.terrain_height_at_point(x as i32, y as i32, &mut height)? {
                height = 0.0;
            }
            file.write_all(&height.to_le_bytes()).map_err(io_error)?;
        }
    }
    file.flush().map_err(io_error)
}

fn has_region_flag(flags: crate::RegionFlags, flag: crate::RegionFlags) -> bool {
    flags.0 & flag.0 == flag.0
}

pub(crate) fn save_region_settings(
    sim: crate::Simulator,
    settings_path: String,
) -> Result<(), Error> {
    let root = PathBuf::from(&settings_path);
    if root.exists() {
        let metadata = fs::symlink_metadata(&root).map_err(io_error)?;
        if metadata.file_type().is_symlink() || !metadata.is_dir() {
            return Err(archive_error("settings output root"));
        }
        fs::remove_dir_all(&root).map_err(io_error)?;
    }
    fs::create_dir_all(&root).map_err(io_error)?;
    let flags = sim.flags;
    let settings = RegionSettings {
        allow_damage: has_region_flag(flags, crate::RegionFlags::ALLOW_DAMAGE),
        allow_land_resell: !has_region_flag(flags, crate::RegionFlags::BLOCK_LAND_RESELL),
        block_fly: has_region_flag(flags, crate::RegionFlags::NO_FLY),
        block_land_show_in_search: has_region_flag(flags, crate::RegionFlags::BLOCK_PARCEL_SEARCH),
        block_terraform: has_region_flag(flags, crate::RegionFlags::BLOCK_TERRAFORM),
        disable_collisions: has_region_flag(flags, crate::RegionFlags::SKIP_COLLISIONS),
        disable_physics: has_region_flag(flags, crate::RegionFlags::SKIP_PHYSICS),
        disable_scripts: has_region_flag(flags, crate::RegionFlags::SKIP_SCRIPTS),
        fixed_sun: has_region_flag(flags, crate::RegionFlags::SUN_FIXED),
        maturity_rating: i32::from(sim.access.0),
        restrict_pushing: has_region_flag(flags, crate::RegionFlags::RESTRICT_PUSH_OBJECT),
        terrain_detail0: sim.terrain_detail0,
        terrain_detail1: sim.terrain_detail1,
        terrain_detail2: sim.terrain_detail2,
        terrain_detail3: sim.terrain_detail3,
        terrain_height_range00: sim.terrain_height_range00,
        terrain_height_range01: sim.terrain_height_range01,
        terrain_height_range10: sim.terrain_height_range10,
        terrain_height_range11: sim.terrain_height_range11,
        terrain_start_height00: sim.terrain_start_height00,
        terrain_start_height01: sim.terrain_start_height01,
        terrain_start_height10: sim.terrain_start_height10,
        terrain_start_height11: sim.terrain_start_height11,
        water_height: sim.water_height,
        ..RegionSettings::default()
    };
    let safe_name = sim.name.replace(['/', '\\'], "_");
    settings.to_xml(
        root.join(format!("{safe_name}.xml"))
            .to_string_lossy()
            .into_owned(),
    )
}

#[cfg(test)]
mod tests {
    use super::{
        BLOCK, MAX_ENTRY_BYTES, MAX_PATH_DEPTH, TarArchiveReader, padded_octal,
        validate_archive_path,
    };
    use crate::assets::TarArchiveReaderTarEntryType;

    #[test]
    fn rejects_absolute_parent_and_windows_style_archive_paths() {
        for path in [
            "/etc/passwd",
            "../escape",
            "assets/../../escape",
            "C:\\escape",
            "assets\\file",
        ] {
            assert!(
                validate_archive_path(path).is_err(),
                "accepted unsafe path {path}"
            );
        }
    }

    #[test]
    fn rejects_archive_paths_over_depth_limit() {
        let path = std::iter::repeat_n("directory", MAX_PATH_DEPTH + 1)
            .collect::<Vec<_>>()
            .join("/");
        assert!(validate_archive_path(&path).is_err());
    }

    #[test]
    fn octal_fields_are_fixed_width_and_nul_terminated() {
        let encoded = padded_octal(0o1234, 8).expect("octal field");
        assert_eq!(encoded.len(), 8);
        assert_eq!(encoded.last(), Some(&0));
        assert_eq!(&encoded[3..7], b"1234");
    }

    #[test]
    fn rejects_declared_entry_over_size_limit_before_reading_payload() {
        let mut header = [0u8; BLOCK];
        header[..9].copy_from_slice(b"large.bin");
        header[100..108].copy_from_slice(&padded_octal(0o644, 8).expect("mode"));
        header[124..136].copy_from_slice(
            &padded_octal((MAX_ENTRY_BYTES + 1) as u64, 12).expect("oversized length"),
        );
        header[148..156].fill(b' ');
        header[156] = b'0';
        header[257..263].copy_from_slice(b"ustar\0");
        header[263..265].copy_from_slice(b"00");
        let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
        header[148..156].copy_from_slice(&padded_octal(checksum, 8).expect("checksum"));
        let reader = TarArchiveReader::new(Box::new(std::io::Cursor::new(header.to_vec())))
            .expect("tar reader");
        let mut path = String::new();
        let mut kind = TarArchiveReaderTarEntryType::TYPE_UNKNOWN;
        assert!(reader.read_entry(&mut path, &mut kind).is_err());
    }
}
