//! Command-line entry point for Component packaging and WIT dependency workflows.
//!
//! Typical release packaging:
//!
//! ```text
//! wasmeld wit fetch --manifest components/counter/wasmeld.toml --locked
//! wasmeld pack components/counter/Cargo.toml --locked
//! ```
//!
//! `wit fetch` resolves registry or local `replace` sources into `wit/deps`
//! and writes `wit.lock`. `--locked` verifies that resolution matches the
//! existing lock file and is the appropriate mode for CI. A `replace` changes
//! only the development source used for a declared package identity; it does
//! not rename that package or make the local path part of the published
//! Component contract.

mod dev;

use std::{
    env, fs,
    path::{Path, PathBuf},
    process::{Command, ExitCode},
};

use serde::Deserialize;
use sha2::{Digest, Sha256};
use wasmeld_package::{
    PackageManifest,
    module::{MODULE_MANIFEST_FILE, ModuleManifest, find_module_manifest, sync_dependencies},
    wit_package::{WitPackageMetadata, build_wit_package},
    write_package,
};

const TARGET: &str = "wasm32-wasip2";

#[derive(Clone, Copy)]
pub(crate) enum BuildProfile {
    /// Fast local build with Cargo's `debug` profile.
    Debug,
    /// Optimized artifact from Cargo's `release` profile.
    Release,
}

impl BuildProfile {
    fn directory(self) -> &'static str {
        match self {
            Self::Debug => "debug",
            Self::Release => "release",
        }
    }
}

pub(crate) enum PackageIdentity {
    /// Use `[package.metadata.wasmeld].id` and the Cargo package version.
    Cargo,
    /// Use a development service ID and a Component-content revision suffix.
    Development { id: Option<String> },
}

pub(crate) struct PackRequest {
    /// Cargo manifest for the Component crate.
    pub manifest_path: PathBuf,
    /// Destination package, or the workspace `dist` default.
    pub output: Option<PathBuf>,
    /// Reuse an artifact already present under Cargo's target directory.
    ///
    /// The caller is responsible for ensuring it matches current sources and
    /// the requested profile.
    pub no_build: bool,
    /// Require `wit.lock` to match dependency resolution exactly.
    pub locked: bool,
    /// Cargo profile used to build and locate the Component.
    pub profile: BuildProfile,
    /// Stable release or content-addressed development identity.
    pub identity: PackageIdentity,
}

pub(crate) struct PackedComponent {
    /// Validated metadata embedded in the `.wasmpkg`.
    pub manifest: PackageManifest,
    /// Path to the resulting `.wasmpkg`.
    pub output: PathBuf,
    /// Path to the raw Component produced or reused by Cargo.
    pub artifact: PathBuf,
}

#[derive(Debug, Deserialize)]
struct CargoMetadata {
    packages: Vec<CargoPackage>,
    target_directory: PathBuf,
    workspace_root: PathBuf,
}

#[derive(Debug, Deserialize)]
struct CargoPackage {
    manifest_path: PathBuf,
    version: String,
    metadata: serde_json::Value,
    targets: Vec<CargoTarget>,
}

#[derive(Debug, Deserialize)]
struct CargoTarget {
    name: String,
    crate_types: Vec<String>,
}

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("wasmeld: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let mut arguments = env::args().skip(1);
    match arguments.next().as_deref() {
        Some("pack") => run_pack(arguments.collect()),
        Some("dev") => dev::run(arguments.collect()),
        Some("wit") => run_wit(arguments.collect()),
        _ => Err(usage().into()),
    }
}

fn run_pack(arguments: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
    let mut arguments = arguments.into_iter();
    let manifest_path = arguments.next().ok_or_else(usage)?;
    let mut output = None;
    let mut no_build = false;
    let mut locked = false;
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--output" | "-o" => {
                output = Some(
                    arguments
                        .next()
                        .map(PathBuf::from)
                        .ok_or("--output requires a path")?,
                );
            }
            "--no-build" => no_build = true,
            "--locked" => locked = true,
            _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()),
        }
    }

    let packed = pack_component(PackRequest {
        manifest_path: PathBuf::from(manifest_path),
        output,
        no_build,
        locked,
        profile: BuildProfile::Release,
        identity: PackageIdentity::Cargo,
    })?;

    println!(
        "packed {}@{} -> {}",
        packed.manifest.id,
        packed.manifest.revision,
        packed.output.display()
    );
    println!("component: {}", packed.artifact.display());
    println!("sha256: {}", packed.manifest.sha256);
    Ok(())
}

pub(crate) fn pack_component(
    request: PackRequest,
) -> Result<PackedComponent, Box<dyn std::error::Error>> {
    let manifest_path = fs::canonicalize(request.manifest_path)?;
    // Materialize WIT before asking Cargo for metadata or compiling: the
    // component's `wit_bindgen::generate!` reads `wit/world.wit` and
    // `wit/deps` during macro expansion.
    sync_component_dependencies(&manifest_path, request.locked)?;
    let metadata = cargo_metadata(&manifest_path)?;
    let package = metadata
        .packages
        .iter()
        .find(|package| package.manifest_path == manifest_path)
        .ok_or("Cargo metadata did not contain the requested package")?;
    let cargo_id = package
        .metadata
        .get("wasmeld")
        .and_then(|metadata| metadata.get("id"))
        .and_then(serde_json::Value::as_str)
        .ok_or("[package.metadata.wasmeld].id is required")?
        .to_owned();
    let world = package
        .metadata
        .get("wasmeld")
        .and_then(|metadata| metadata.get("world"))
        .and_then(serde_json::Value::as_str)
        .ok_or("[package.metadata.wasmeld].world is required")?
        .to_owned();
    let cargo_revision = package.version.clone();
    let target = package
        .targets
        .iter()
        .find(|target| target.crate_types.iter().any(|kind| kind == "cdylib"))
        .ok_or("component package must expose a cdylib target")?
        .name
        .clone();

    if !request.no_build {
        build_component(&manifest_path, request.profile)?;
    }

    let artifact = metadata
        .target_directory
        .join(TARGET)
        .join(request.profile.directory())
        .join(format!("{}.wasm", target.replace('-', "_")));
    let component = fs::read(&artifact).map_err(|error| {
        format!(
            "failed to read built component {}: {error}",
            artifact.display()
        )
    })?;
    let (id, revision) = match request.identity {
        PackageIdentity::Cargo => (cargo_id, cargo_revision),
        PackageIdentity::Development { id } => {
            // The revision changes only when compiled Component bytes change.
            // This makes registration retryable and avoids mutating an
            // immutable identity while editing.
            let digest = format!("{:x}", Sha256::digest(&component));
            (
                id.unwrap_or_else(|| format!("{cargo_id}-dev")),
                format!("{cargo_revision}-dev.h{}", &digest[..12]),
            )
        }
    };
    PackageManifest::new(&id, &revision, &world, b"identity-validation").validate()?;
    let output = request.output.unwrap_or_else(|| {
        metadata
            .workspace_root
            .join("dist")
            .join(format!("{id}-{revision}.wasmpkg"))
    });
    if let Some(parent) = output.parent() {
        fs::create_dir_all(parent)?;
    }
    let file = fs::File::create(&output)?;
    let manifest = write_package(file, id, revision, world, &component)?;
    Ok(PackedComponent {
        manifest,
        output,
        artifact,
    })
}

fn run_wit(arguments: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
    let mut arguments = arguments.into_iter();
    match arguments.next().as_deref() {
        Some("fetch" | "tidy") => {
            let options = parse_wit_sync_options(arguments)?;
            let manifest = resolve_module_manifest(options.manifest)?;
            print_sync_report(&manifest, options.locked)
        }
        Some("graph") => {
            let manifest = resolve_module_manifest(parse_manifest_option(arguments)?)?;
            print_dependency_graph(&manifest)
        }
        Some("replace") => run_wit_replace(arguments),
        Some("build") => run_wit_build(arguments),
        Some("publish") => run_wit_publish(arguments),
        _ => Err(usage().into()),
    }
}

struct WitSyncOptions {
    manifest: Option<PathBuf>,
    locked: bool,
}

fn parse_wit_sync_options(
    mut arguments: impl Iterator<Item = String>,
) -> Result<WitSyncOptions, Box<dyn std::error::Error>> {
    let mut manifest = None;
    let mut locked = false;
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--manifest" => {
                manifest = Some(
                    arguments
                        .next()
                        .map(PathBuf::from)
                        .ok_or("--manifest requires a path")?,
                );
            }
            "--locked" => locked = true,
            _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()),
        }
    }
    Ok(WitSyncOptions { manifest, locked })
}

fn parse_manifest_option(
    mut arguments: impl Iterator<Item = String>,
) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
    let mut manifest = None;
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--manifest" => {
                manifest = Some(
                    arguments
                        .next()
                        .map(PathBuf::from)
                        .ok_or("--manifest requires a path")?,
                );
            }
            _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()),
        }
    }
    Ok(manifest)
}

fn run_wit_replace(
    mut arguments: impl Iterator<Item = String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let package = arguments.next().ok_or("wit replace requires a package")?;
    let mut manifest_path = None;
    let mut replacement_path = None;
    let mut drop = false;
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--manifest" => {
                manifest_path = Some(
                    arguments
                        .next()
                        .map(PathBuf::from)
                        .ok_or("--manifest requires a path")?,
                );
            }
            "--path" => {
                replacement_path = Some(
                    arguments
                        .next()
                        .map(PathBuf::from)
                        .ok_or("--path requires a directory")?,
                );
            }
            "--drop" => drop = true,
            _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()),
        }
    }
    if drop == replacement_path.is_some() {
        return Err("wit replace requires exactly one of --path or --drop".into());
    }

    let manifest_path = resolve_module_manifest(manifest_path)?;
    let mut manifest = ModuleManifest::read(&manifest_path)?;
    // Replace is recorded in the source manifest. The next fetch/pack resolves
    // and locks its contents; generated `wit/deps` must not be hand-edited.
    if drop {
        if !manifest.drop_replacement(&package) {
            return Err(format!("replacement {package:?} does not exist").into());
        }
        manifest.write(&manifest_path)?;
        println!("dropped replace {package}");
    } else {
        let replacement_path = replacement_path.expect("validated above");
        manifest.set_path_replacement(&package, &replacement_path)?;
        manifest.write(&manifest_path)?;
        println!("replaced {package} => path+{}", replacement_path.display());
    }
    println!("manifest: {}", manifest_path.display());
    Ok(())
}

fn run_wit_build(
    mut arguments: impl Iterator<Item = String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let source = arguments.next().ok_or("wit build requires a source path")?;
    let mut output = None;
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--output" | "-o" => {
                output = Some(
                    arguments
                        .next()
                        .map(PathBuf::from)
                        .ok_or("--output requires a path")?,
                );
            }
            _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()),
        }
    }
    let package = build_wit_package(&source)?;
    let output = output.unwrap_or_else(|| {
        let basename = package.metadata.name.replace(':', "-");
        PathBuf::from("dist/wit").join(format!("{basename}-{}.wasm", package.metadata.version))
    });
    if let Some(parent) = output.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&output, &package.bytes)?;
    println!(
        "built {}@{} -> {}",
        package.metadata.name,
        package.metadata.version,
        output.display()
    );
    println!("sha256: {}", package.metadata.sha256);
    Ok(())
}

fn run_wit_publish(
    mut arguments: impl Iterator<Item = String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let source = arguments
        .next()
        .ok_or("wit publish requires a source path")?;
    let mut registry = env::var("WASMELD_REGISTRY").ok();
    while let Some(argument) = arguments.next() {
        match argument.as_str() {
            "--registry" => {
                registry = Some(
                    arguments
                        .next()
                        .ok_or("--registry requires an HTTP(S) URL")?,
                );
            }
            _ => return Err(format!("unknown argument {argument:?}\n{}", usage()).into()),
        }
    }
    let registry = registry
        .ok_or("wit publish requires --registry or the WASMELD_REGISTRY environment variable")?;
    let registry = registry.trim_end_matches('/');
    let package = build_wit_package(&source)?;
    let url = format!("{registry}/api/v1/wit/packages");
    let part = reqwest::blocking::multipart::Part::bytes(package.bytes)
        .file_name("package.wasm")
        .mime_str("application/wasm")?;
    let response = reqwest::blocking::Client::builder()
        .user_agent(format!("wasmeld/{}", env!("CARGO_PKG_VERSION")))
        .build()?
        .post(&url)
        .multipart(reqwest::blocking::multipart::Form::new().part("package", part))
        .send()?;
    let status = response.status();
    if !status.is_success() {
        return Err(format!("registry returned HTTP {status}: {}", response.text()?).into());
    }
    let published = response.json::<WitPackageMetadata>()?;
    // A successful status is insufficient: compare the server's canonical
    // identity and digest to detect a proxy or Registry returning metadata for
    // different bytes.
    if published != package.metadata {
        return Err(format!(
            "registry metadata mismatch: expected {}@{} ({})",
            package.metadata.name, package.metadata.version, package.metadata.sha256
        )
        .into());
    }
    println!(
        "published {}@{} -> {registry}",
        published.name, published.version
    );
    println!("sha256: {}", published.sha256);
    Ok(())
}

fn resolve_module_manifest(
    manifest: Option<PathBuf>,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
    match manifest {
        Some(path) => Ok(fs::canonicalize(path)?),
        None => Ok(find_module_manifest(env::current_dir()?)?),
    }
}

fn sync_component_dependencies(
    cargo_manifest: &Path,
    locked: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let module_manifest = cargo_manifest.with_file_name(MODULE_MANIFEST_FILE);
    if module_manifest.is_file() {
        print_sync_report(&module_manifest, locked)?;
    }
    Ok(())
}

fn print_sync_report(manifest: &Path, locked: bool) -> Result<(), Box<dyn std::error::Error>> {
    let report = sync_dependencies(manifest, locked)?;
    let action = if locked { "verified" } else { "resolved" };
    println!(
        "{action} {} WIT package(s) -> {}",
        report.packages.len(),
        report.wit_root.join("deps").display()
    );
    for package in report.packages {
        println!(
            "  {}@{} => {} ({})",
            package.name, package.version, package.source, package.sha256
        );
    }
    println!("lock: {}", report.lock_path.display());
    Ok(())
}

fn print_dependency_graph(manifest_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let manifest = ModuleManifest::read(manifest_path)?;
    println!("{}", manifest_path.display());
    for (name, version) in &manifest.dependencies {
        let exact = format!("{name}@{version}");
        let replacement = manifest
            .replacements
            .get(&exact)
            .or_else(|| manifest.replacements.get(name));
        match replacement {
            Some(replacement) => {
                println!("  {exact} => path+{}", replacement.path.display());
            }
            None => match &manifest.registry.url {
                Some(registry) => println!("  {exact} => registry+{registry}"),
                None => println!("  {exact} => registry (unconfigured)"),
            },
        }
    }
    Ok(())
}

fn cargo_metadata(manifest_path: &Path) -> Result<CargoMetadata, Box<dyn std::error::Error>> {
    let output = Command::new("cargo")
        .args(["metadata", "--format-version", "1", "--no-deps"])
        .arg("--manifest-path")
        .arg(manifest_path)
        .output()?;
    if !output.status.success() {
        return Err(format!(
            "cargo metadata failed:\n{}",
            String::from_utf8_lossy(&output.stderr)
        )
        .into());
    }
    Ok(serde_json::from_slice(&output.stdout)?)
}

fn build_component(
    manifest_path: &Path,
    profile: BuildProfile,
) -> Result<(), Box<dyn std::error::Error>> {
    let toolchain = env::var("WASMELD_COMPONENT_TOOLCHAIN").unwrap_or_else(|_| "1.90.0".to_owned());
    let mut command = Command::new("rustup");
    command
        .args(["run", &toolchain, "cargo", "build"])
        .arg("--manifest-path")
        .arg(manifest_path)
        .args(["--target", TARGET]);
    if matches!(profile, BuildProfile::Release) {
        command.arg("--release");
    }
    let status = command.status()?;
    if !status.success() {
        return Err(format!("component build failed with status {status}").into());
    }
    Ok(())
}

fn usage() -> String {
    "usage:
  wasmeld pack <Cargo.toml> [--output <file.wasmpkg>] [--no-build] [--locked]
  wasmeld dev <Cargo.toml> [--console <url>] [--id <service-id>] [--locked] [--release] [--once] [--poll-ms <milliseconds>]
  wasmeld wit fetch [--manifest <wasmeld.toml>] [--locked]
  wasmeld wit tidy [--manifest <wasmeld.toml>] [--locked]
  wasmeld wit graph [--manifest <wasmeld.toml>]
  wasmeld wit replace <package[@version]> --path <directory> [--manifest <wasmeld.toml>]
  wasmeld wit replace <package[@version]> --drop [--manifest <wasmeld.toml>]
  wasmeld wit build <wit-path> [--output <package.wasm>]
  wasmeld wit publish <wit-path> --registry <url>"
        .to_owned()
}
