//! Standard binary WIT package encoding and inspection.
//!
//! Package identity is derived from the encoded WIT itself. Registry clients
//! never submit a separate package name, version, or dependency manifest that
//! could disagree with the artifact.

use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use wit_parser::{
    Resolve,
    decoding::{DecodedWasm, decode},
};

/// Metadata schema returned by the Wasmeld WIT Registry API.
pub const WIT_PACKAGE_SCHEMA_VERSION: u32 = 1;

/// One exact, direct WIT package dependency.
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WitDependency {
    /// Package identity in `namespace:name` form.
    pub name: String,
    /// Exact semantic version encoded by the dependency package.
    pub version: String,
}

/// Metadata derived from a binary WIT package.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WitPackageMetadata {
    /// Metadata response schema used by the Registry API.
    pub schema_version: u32,
    /// Package identity derived from binary WIT contents.
    pub name: String,
    /// Explicit semantic version derived from binary WIT contents.
    pub version: String,
    /// SHA-256 digest of the complete binary WIT artifact.
    pub sha256: String,
    /// Exact direct dependencies embedded in the package.
    pub dependencies: Vec<WitDependency>,
}

/// Encoded package bytes together with metadata derived from those bytes.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BuiltWitPackage {
    /// Metadata derived by decoding the bytes after encoding.
    pub metadata: WitPackageMetadata,
    /// Standard Component Model binary WIT package.
    pub bytes: Vec<u8>,
}

/// Errors raised while parsing WIT source or decoding a binary WIT package.
#[derive(Debug, Error)]
pub enum WitPackageError {
    /// WIT source could not be parsed or resolved.
    #[error("failed to parse WIT package at {path}: {message}")]
    Source {
        /// Source file or directory passed to the parser.
        path: PathBuf,
        /// Parser or dependency-resolution detail.
        message: String,
    },

    /// A resolved WIT package could not be encoded.
    #[error("failed to encode WIT package: {0}")]
    Encode(String),

    /// Input bytes are not a standard binary WIT package.
    #[error("invalid binary WIT package: {0}")]
    Decode(String),

    /// The package identity omits the version required by the Registry.
    #[error("WIT package {0} must declare an explicit semantic version")]
    MissingVersion(String),
}

/// Parses a WIT source path and encodes its main package as binary WIT.
///
/// The package must declare an explicit semantic version. Dependencies
/// available through the source path are encoded as Component Model package
/// references and reported in [`WitPackageMetadata::dependencies`].
///
/// # Errors
///
/// Returns an error when WIT parsing, dependency resolution, binary encoding,
/// or post-encode inspection fails.
pub fn build_wit_package(path: impl AsRef<Path>) -> Result<BuiltWitPackage, WitPackageError> {
    let path = path.as_ref();
    let mut resolve = Resolve::default();
    let (package, _) = resolve
        .push_path(path)
        .map_err(|error| WitPackageError::Source {
            path: path.to_path_buf(),
            message: error.to_string(),
        })?;
    let bytes = wit_component::encode(&resolve, package)
        .map_err(|error| WitPackageError::Encode(error.to_string()))?;
    let metadata = metadata_from_resolve(&resolve, package, &bytes)?;
    Ok(BuiltWitPackage { metadata, bytes })
}

/// Decodes binary WIT and derives its identity, direct dependencies, and digest.
///
/// Ordinary WebAssembly Components are rejected even though both formats use a
/// Component Model binary container.
///
/// # Errors
///
/// Returns an error if the bytes are malformed, contain a Component instead of
/// a WIT package, or any package in the direct dependency set lacks a version.
pub fn inspect_wit_package(bytes: &[u8]) -> Result<WitPackageMetadata, WitPackageError> {
    let decoded = decode(bytes).map_err(|error| WitPackageError::Decode(error.to_string()))?;
    let DecodedWasm::WitPackage(resolve, package) = decoded else {
        return Err(WitPackageError::Decode(
            "artifact is a WebAssembly Component, not a binary WIT package".to_owned(),
        ));
    };
    metadata_from_resolve(&resolve, package, bytes)
}

/// Returns the lowercase SHA-256 digest used as the Registry artifact identity.
pub fn wit_package_sha256(bytes: &[u8]) -> String {
    format!("{:x}", Sha256::digest(bytes))
}

fn metadata_from_resolve(
    resolve: &Resolve,
    package: wit_parser::PackageId,
    bytes: &[u8],
) -> Result<WitPackageMetadata, WitPackageError> {
    let package_name = &resolve.packages[package].name;
    let name = format!("{}:{}", package_name.namespace, package_name.name);
    let version = package_name
        .version
        .as_ref()
        .map(ToString::to_string)
        .ok_or_else(|| WitPackageError::MissingVersion(name.clone()))?;
    let mut dependencies = resolve
        .package_direct_deps(package)
        .map(|dependency| {
            let dependency = &resolve.packages[dependency].name;
            let dependency_name = format!("{}:{}", dependency.namespace, dependency.name);
            let dependency_version = dependency
                .version
                .as_ref()
                .map(ToString::to_string)
                .ok_or_else(|| WitPackageError::MissingVersion(dependency_name.clone()))?;
            Ok(WitDependency {
                name: dependency_name,
                version: dependency_version,
            })
        })
        .collect::<Result<Vec<_>, WitPackageError>>()?;
    dependencies.sort();
    dependencies.dedup();

    Ok(WitPackageMetadata {
        schema_version: WIT_PACKAGE_SCHEMA_VERSION,
        name,
        version,
        sha256: wit_package_sha256(bytes),
        dependencies,
    })
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    #[test]
    fn builds_and_inspects_a_binary_wit_package() {
        let source = tempdir().unwrap();
        fs::write(
            source.path().join("package.wit"),
            "package wasmeld:clock@1.2.3;\ninterface clock { now: func() -> u64; }\n",
        )
        .unwrap();

        let package = build_wit_package(source.path()).unwrap();
        let inspected = inspect_wit_package(&package.bytes).unwrap();

        assert_eq!(package.metadata, inspected);
        assert_eq!(inspected.name, "wasmeld:clock");
        assert_eq!(inspected.version, "1.2.3");
        assert!(inspected.dependencies.is_empty());
        assert_eq!(inspected.sha256.len(), 64);
    }

    #[test]
    fn rejects_a_component_as_a_wit_package() {
        let error = inspect_wit_package(b"not wasm").unwrap_err();
        assert!(matches!(error, WitPackageError::Decode(_)));
    }
}
