//! Bounded, offline command implementation for the `OSDInspector` tool.

use clap::{Parser, Subcommand, ValueEnum};
use libremetaverse::structured_data::{OSD, OSDParser, OSDType};
use libremetaverse::types::{Material, PCode, PathCurve, ProfileCurve, Quaternion, UUID, Vector3};
use libremetaverse::{Primitive, PrimitiveConstructionData, PrimitiveObjectProperties};
use std::fmt;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;

/// Successful command completion.
pub const EXIT_SUCCESS: u8 = 0;
/// Command-line usage errors. Clap also uses this value for invalid arguments.
pub const EXIT_USAGE: u8 = 2;
/// File or standard-stream I/O failed.
pub const EXIT_IO: u8 = 3;
/// Input was too large or was not valid OSD.
pub const EXIT_INVALID_OSD: u8 = 4;
/// Valid OSD could not be interpreted as a primitive.
pub const EXIT_INVALID_PRIMITIVE: u8 = 5;

const DEFAULT_MAX_INPUT_BYTES: u64 = OSD::DEFAULT_MAX_BINARY_BYTES as u64;

#[derive(Debug, Parser)]
#[command(
    name = "osd-inspector",
    version,
    about = "Inspect, convert, and validate LibreMetaverse structured data offline",
    long_about = None,
    arg_required_else_help = true
)]
pub struct Cli {
    /// Maximum accepted input size. Parsing also enforces the library depth and node limits.
    #[arg(long, global = true, default_value_t = DEFAULT_MAX_INPUT_BYTES, value_name = "BYTES")]
    max_input_bytes: u64,

    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Display an OSD value's type and recursively formatted structure.
    #[command(alias = "i")]
    Inspect {
        /// Input file, or '-' for standard input.
        input: PathBuf,
    },
    /// Convert OSD to JSON, XML, binary LLSD, or notation LLSD.
    #[command(alias = "c")]
    Convert {
        /// Input file, or '-' for standard input.
        input: PathBuf,
        /// Output format (json/j, xml/x, binary/bin/b, notation/llsd/n).
        format: OutputFormat,
        /// Output file, or '-' for standard output.
        output: PathBuf,
    },
    /// Validate an OSD value and report its root shape.
    #[command(alias = "v")]
    Validate {
        /// Input file, or '-' for standard input.
        input: PathBuf,
    },
    /// Emit a deterministic sample cube as pretty JSON.
    PrimToOsd,
    /// Parse an OSD value as a Primitive and summarize it.
    OsdToPrim {
        /// Input file, or '-' for standard input.
        input: PathBuf,
    },
}

#[derive(Clone, Copy, Debug, ValueEnum)]
enum OutputFormat {
    #[value(alias = "j")]
    Json,
    #[value(alias = "x")]
    Xml,
    #[value(alias = "bin", alias = "b")]
    Binary,
    #[value(alias = "llsd", alias = "n")]
    Notation,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum InputFormat {
    Json,
    Xml,
    Binary,
    Notation,
}

#[derive(Debug)]
enum InspectorError {
    Io { action: String, source: io::Error },
    InputTooLarge { limit: u64 },
    InvalidOsd { input: String },
    Serialization { format: OutputFormat },
    InvalidPrimitive,
}

impl InspectorError {
    const fn exit_code(&self) -> u8 {
        match self {
            Self::Io { .. } => EXIT_IO,
            Self::InputTooLarge { .. } | Self::InvalidOsd { .. } => EXIT_INVALID_OSD,
            Self::Serialization { .. } | Self::InvalidPrimitive => EXIT_INVALID_PRIMITIVE,
        }
    }
}

impl fmt::Display for InspectorError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io { action, source } => write!(formatter, "{action}: {source}"),
            Self::InputTooLarge { limit } => {
                write!(formatter, "input exceeds the configured {limit}-byte limit")
            }
            Self::InvalidOsd { input } => write!(formatter, "invalid OSD input: {input}"),
            Self::Serialization { format } => {
                write!(formatter, "could not serialize OSD as {format:?}")
            }
            Self::InvalidPrimitive => write!(formatter, "OSD value is not a Primitive map"),
        }
    }
}

/// Parses command-line arguments with clap and runs the offline tool.
#[must_use]
pub fn main_entry() -> ExitCode {
    let cli = Cli::parse();
    let stdin = io::stdin();
    let stdout = io::stdout();
    let stderr = io::stderr();
    let mut input = stdin.lock();
    let mut output = stdout.lock();
    let mut errors = stderr.lock();
    match execute(cli, &mut input, &mut output) {
        Ok(()) => ExitCode::from(EXIT_SUCCESS),
        Err(error) => {
            let _ = writeln!(errors, "osd-inspector: {error}");
            ExitCode::from(error.exit_code())
        }
    }
}

fn execute(cli: Cli, stdin: &mut dyn Read, stdout: &mut dyn Write) -> Result<(), InspectorError> {
    if cli.max_input_bytes == 0 {
        return Err(InspectorError::InputTooLarge { limit: 0 });
    }
    match cli.command {
        Command::Inspect { input } => inspect(&input, cli.max_input_bytes, stdin, stdout),
        Command::Convert {
            input,
            format,
            output,
        } => convert(&input, format, &output, cli.max_input_bytes, stdin, stdout),
        Command::Validate { input } => validate(&input, cli.max_input_bytes, stdin, stdout),
        Command::PrimToOsd => primitive_to_osd(stdout),
        Command::OsdToPrim { input } => {
            osd_to_primitive(&input, cli.max_input_bytes, stdin, stdout)
        }
    }
}

fn inspect(
    input: &Path,
    limit: u64,
    stdin: &mut dyn Read,
    output: &mut dyn Write,
) -> Result<(), InspectorError> {
    let bytes = read_input(input, limit, stdin)?;
    let osd = parse_osd(&bytes, input)?;
    writeln!(output, "File: {}", display_path(input)).map_err(stdout_error)?;
    writeln!(output, "Type: {}", type_name(osd.type_())).map_err(stdout_error)?;
    writeln!(output, "Size: {} bytes", bytes.len()).map_err(stdout_error)?;
    writeln!(output, "\nStructure:\n{}", "-".repeat(60)).map_err(stdout_error)?;
    display_osd(&osd, 0, output)?;
    writeln!(output, "{}", "-".repeat(60)).map_err(stdout_error)
}

fn convert(
    input: &Path,
    format: OutputFormat,
    destination: &Path,
    limit: u64,
    stdin: &mut dyn Read,
    stdout: &mut dyn Write,
) -> Result<(), InspectorError> {
    let bytes = read_input(input, limit, stdin)?;
    let osd = parse_osd(&bytes, input)?;
    let encoded = serialize(osd, format)?;
    if is_standard_stream(destination) {
        stdout.write_all(&encoded).map_err(stdout_error)?;
        stdout.flush().map_err(stdout_error)?;
    } else {
        write_file(destination, &encoded)?;
        writeln!(
            stdout,
            "Converted {} to {}: {} ({} bytes)",
            display_path(input),
            format_name(format),
            destination.display(),
            encoded.len()
        )
        .map_err(stdout_error)?;
    }
    Ok(())
}

fn validate(
    input: &Path,
    limit: u64,
    stdin: &mut dyn Read,
    output: &mut dyn Write,
) -> Result<(), InspectorError> {
    let bytes = read_input(input, limit, stdin)?;
    let osd = parse_osd(&bytes, input)?;
    writeln!(output, "Valid OSD file").map_err(stdout_error)?;
    writeln!(output, "  Type: {}", type_name(osd.type_())).map_err(stdout_error)?;
    match osd {
        OSD::Map(values) => writeln!(output, "  Keys: {}", values.len()).map_err(stdout_error)?,
        OSD::Array(values) => {
            writeln!(output, "  Elements: {}", values.len()).map_err(stdout_error)?;
        }
        _ => {}
    }
    Ok(())
}

fn primitive_to_osd(output: &mut dyn Write) -> Result<(), InspectorError> {
    let osd = sample_primitive()
        .and_then(|primitive| primitive.get_osd())
        .map_err(|_| InspectorError::InvalidPrimitive)?;
    let json = OSDParser::serialize_json_string(osd, Some(true)).map_err(|_| {
        InspectorError::Serialization {
            format: OutputFormat::Json,
        }
    })?;
    writeln!(output, "{json}").map_err(stdout_error)
}

fn osd_to_primitive(
    input: &Path,
    limit: u64,
    stdin: &mut dyn Read,
    output: &mut dyn Write,
) -> Result<(), InspectorError> {
    let bytes = read_input(input, limit, stdin)?;
    let osd = parse_osd(&bytes, input)?;
    if !matches!(osd, OSD::Map(_)) {
        return Err(InspectorError::InvalidPrimitive);
    }
    let primitive = Primitive::from_osd(osd).map_err(|_| InspectorError::InvalidPrimitive)?;
    let name = primitive
        .properties
        .as_ref()
        .map_or("Unknown", |properties| properties.name.as_str());
    writeln!(output, "Successfully parsed Primitive:").map_err(stdout_error)?;
    writeln!(output, "  ID: {}", primitive.id).map_err(stdout_error)?;
    writeln!(output, "  Name: {name}").map_err(stdout_error)?;
    writeln!(output, "  Type: {:?}", primitive.type_()).map_err(stdout_error)?;
    writeln!(output, "  Position: {}", primitive.position.to_string()).map_err(stdout_error)?;
    writeln!(output, "  Scale: {}", primitive.scale.to_string()).map_err(stdout_error)?;
    writeln!(output, "  Material: {:?}", primitive.prim_data.material).map_err(stdout_error)?;
    writeln!(output, "  PCode: {:?}", primitive.prim_data.p_code).map_err(stdout_error)
}

fn sample_primitive() -> Result<Primitive, libremetaverse::Error> {
    let mut primitive = Primitive::new_with_constructor()?;
    primitive.id = UUID::new_with_string("11111111-2222-3333-4444-555555555555".into())?;
    primitive.local_id = 12_345;
    primitive.position = Vector3 {
        x: 128.0,
        y: 128.0,
        z: 25.0,
    };
    primitive.rotation = Quaternion::identity();
    primitive.scale = Vector3 {
        x: 1.0,
        y: 1.0,
        z: 1.0,
    };
    let mut construction = PrimitiveConstructionData::new_with_constructor()?;
    construction.p_code = PCode::Prim;
    construction.material = Material::Wood;
    construction.path_curve = PathCurve::Line;
    construction.path_scale_x = 1.0;
    construction.path_scale_y = 1.0;
    construction.set_profile_curve_with_property(ProfileCurve::Square);
    primitive.prim_data = construction;
    let mut properties = PrimitiveObjectProperties::new()?;
    properties.name = "Example Cube".into();
    properties.description = "A simple cube created with LibreMetaverse".into();
    primitive.properties = Some(properties);
    Ok(primitive)
}

fn read_input(path: &Path, limit: u64, stdin: &mut dyn Read) -> Result<Vec<u8>, InspectorError> {
    if is_standard_stream(path) {
        return read_bounded(stdin, limit, "reading standard input");
    }
    let file = File::open(path).map_err(|source| InspectorError::Io {
        action: format!("opening {}", path.display()),
        source,
    })?;
    if file.metadata().map_or(0, |value| value.len()) > limit {
        return Err(InspectorError::InputTooLarge { limit });
    }
    read_bounded(
        &mut BufReader::new(file),
        limit,
        &format!("reading {}", path.display()),
    )
}

fn read_bounded(input: &mut dyn Read, limit: u64, action: &str) -> Result<Vec<u8>, InspectorError> {
    let capacity = usize::try_from(limit.min(64 * 1024)).unwrap_or(64 * 1024);
    let mut bytes = Vec::with_capacity(capacity);
    input
        .take(limit.saturating_add(1))
        .read_to_end(&mut bytes)
        .map_err(|source| InspectorError::Io {
            action: action.into(),
            source,
        })?;
    if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > limit {
        return Err(InspectorError::InputTooLarge { limit });
    }
    Ok(bytes)
}

fn write_file(path: &Path, bytes: &[u8]) -> Result<(), InspectorError> {
    let file = File::create(path).map_err(|source| InspectorError::Io {
        action: format!("creating {}", path.display()),
        source,
    })?;
    let mut writer = BufWriter::new(file);
    writer
        .write_all(bytes)
        .and_then(|()| writer.flush())
        .map_err(|source| InspectorError::Io {
            action: format!("writing {}", path.display()),
            source,
        })
}

fn parse_osd(bytes: &[u8], path: &Path) -> Result<OSD, InspectorError> {
    if bytes.is_empty() || bytes.iter().all(u8::is_ascii_whitespace) {
        return Err(InspectorError::InvalidOsd {
            input: display_path(path),
        });
    }
    let hinted = format_hint(path, bytes);
    let mut attempts = vec![hinted];
    for format in [
        InputFormat::Json,
        InputFormat::Xml,
        InputFormat::Binary,
        InputFormat::Notation,
    ] {
        if !attempts.contains(&format) {
            attempts.push(format);
        }
    }
    for format in attempts {
        if let Ok(value) = parse_as(bytes, format) {
            return Ok(value);
        }
    }
    Err(InspectorError::InvalidOsd {
        input: display_path(path),
    })
}

fn parse_as(bytes: &[u8], format: InputFormat) -> Result<OSD, libremetaverse::Error> {
    match format {
        InputFormat::Json => OSDParser::deserialize_json_with_string(
            String::from_utf8(bytes.to_vec()).map_err(|_| libremetaverse::Error::Argument)?,
        ),
        InputFormat::Xml => OSDParser::deserialize_llsd_xml_with_bytes(bytes.to_vec()),
        InputFormat::Binary => OSDParser::deserialize_llsd_binary_with_bytes(bytes.to_vec()),
        InputFormat::Notation => OSDParser::deserialize_llsd_notation_with_string(
            String::from_utf8(bytes.to_vec()).map_err(|_| libremetaverse::Error::Argument)?,
        ),
    }
}

fn format_hint(path: &Path, bytes: &[u8]) -> InputFormat {
    match path
        .extension()
        .and_then(|extension| extension.to_str())
        .map(str::to_ascii_lowercase)
        .as_deref()
    {
        Some("json") => InputFormat::Json,
        Some("xml") => InputFormat::Xml,
        Some("bin" | "binary") => InputFormat::Binary,
        Some("notation" | "nt") => InputFormat::Notation,
        _ => sniff_format(bytes),
    }
}

fn sniff_format(bytes: &[u8]) -> InputFormat {
    let text = String::from_utf8_lossy(&bytes[..bytes.len().min(64)]);
    let trimmed = text.trim_start_matches('\u{feff}').trim_start();
    if trimmed.starts_with("<? llsd/binary ?>") || trimmed.starts_with("<?llsd/binary?>") {
        InputFormat::Binary
    } else if trimmed.starts_with('<') {
        InputFormat::Xml
    } else if matches!(
        trimmed.as_bytes().first(),
        Some(b'{' | b'[' | b'"' | b'-' | b'0'..=b'9')
    ) || trimmed.starts_with("true")
        || trimmed.starts_with("false")
        || trimmed.starts_with("null")
    {
        InputFormat::Json
    } else {
        InputFormat::Notation
    }
}

fn serialize(osd: OSD, format: OutputFormat) -> Result<Vec<u8>, InspectorError> {
    let result = match format {
        OutputFormat::Json => {
            OSDParser::serialize_json_string(osd, Some(true)).map(String::into_bytes)
        }
        OutputFormat::Xml => OSDParser::serialize_llsd_xml_bytes(osd),
        OutputFormat::Binary => OSDParser::serialize_llsd_binary_with_osd(osd),
        OutputFormat::Notation => OSDParser::serialize_llsd_notation(osd).map(String::into_bytes),
    };
    result.map_err(|_| InspectorError::Serialization { format })
}

fn display_osd(osd: &OSD, indent: usize, output: &mut dyn Write) -> Result<(), InspectorError> {
    let prefix = "  ".repeat(indent);
    match osd {
        OSD::Map(values) => {
            writeln!(output, "{prefix}Map ({} keys)", values.len()).map_err(stdout_error)?;
            let mut entries = values.iter().collect::<Vec<_>>();
            entries.sort_unstable_by_key(|(key, _)| *key);
            for (key, value) in entries {
                write!(output, "{prefix}  {key}: ").map_err(stdout_error)?;
                if matches!(value, OSD::Map(_) | OSD::Array(_)) {
                    writeln!(output).map_err(stdout_error)?;
                    display_osd(value, indent + 2, output)?;
                } else {
                    display_osd(value, 0, output)?;
                }
            }
        }
        OSD::Array(values) => {
            writeln!(output, "{prefix}Array ({} elements)", values.len()).map_err(stdout_error)?;
            for (index, value) in values.iter().enumerate() {
                write!(output, "{prefix}  [{index}]: ").map_err(stdout_error)?;
                if matches!(value, OSD::Map(_) | OSD::Array(_)) {
                    writeln!(output).map_err(stdout_error)?;
                    display_osd(value, indent + 2, output)?;
                } else {
                    display_osd(value, 0, output)?;
                }
            }
        }
        OSD::String(value) => writeln!(output, "String: {value:?}").map_err(stdout_error)?,
        OSD::Integer(value) => writeln!(output, "Integer: {value}").map_err(stdout_error)?,
        OSD::Real(value) => writeln!(output, "Real: {value}").map_err(stdout_error)?,
        OSD::Boolean(value) => writeln!(output, "Boolean: {value}").map_err(stdout_error)?,
        OSD::UUID(value) => writeln!(output, "UUID: {value}").map_err(stdout_error)?,
        OSD::Date(_) => writeln!(output, "Date: {}", osd.as_string().unwrap_or_default())
            .map_err(stdout_error)?,
        OSD::Uri(value) => writeln!(output, "URI: {}", value.0).map_err(stdout_error)?,
        OSD::Binary(value) => {
            writeln!(output, "Binary: {} bytes", value.len()).map_err(stdout_error)?;
        }
        OSD::LlsdXml(value) => writeln!(output, "LLSD XML: {value:?}").map_err(stdout_error)?,
        OSD::Undefined => writeln!(output, "Unknown (Undefined)").map_err(stdout_error)?,
        _ => writeln!(output, "Unknown ({:?})", osd.type_()).map_err(stdout_error)?,
    }
    Ok(())
}

const fn type_name(value: OSDType) -> &'static str {
    match value {
        OSDType::Unknown => "Unknown",
        OSDType::Boolean => "Boolean",
        OSDType::Integer => "Integer",
        OSDType::Real => "Real",
        OSDType::String => "String",
        OSDType::UUID => "UUID",
        OSDType::Date => "Date",
        OSDType::URI => "URI",
        OSDType::Binary => "Binary",
        OSDType::Map => "Map",
        OSDType::Array => "Array",
        OSDType::LlsdXml => "LlsdXml",
    }
}

const fn format_name(format: OutputFormat) -> &'static str {
    match format {
        OutputFormat::Json => "json",
        OutputFormat::Xml => "xml",
        OutputFormat::Binary => "binary",
        OutputFormat::Notation => "notation",
    }
}

fn is_standard_stream(path: &Path) -> bool {
    path.as_os_str() == "-"
}

fn display_path(path: &Path) -> String {
    if is_standard_stream(path) {
        "<stdin>".into()
    } else {
        path.display().to_string()
    }
}

fn stdout_error(source: io::Error) -> InspectorError {
    InspectorError::Io {
        action: "writing standard output".into(),
        source,
    }
}
