//! Quantum‑Sign CLI (hardened skeleton).
//! Pure-Rust, crates.io-only, no network activity in the sign hotpath.
#![forbid(unsafe_code)]
#![deny(missing_docs)]

use std::collections::BTreeMap;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::Write as _;
use std::fs;
use std::io::{self, Write};
use std::process;
use std::str::FromStr;

use camino::{Utf8Path, Utf8PathBuf};
use clap::{Parser, Subcommand};
use quantum_sign::crypto as qs_crypto;
use quantum_sign::crypto::{DigestAlg, HmacSha512Drbg};
use quantum_sign::format::QSig;
use quantum_sign::policy as qs_policy;
use quantum_sign::policy::{Policy, ValidationError};
use serde::Serialize;
use sha2::{Digest, Sha256, Sha512};
use sha3::{
    digest::{ExtendableOutput, XofReader},
    Shake256,
};

#[path = "../quorum.rs"]
mod quorum;

const MAX_SIGNERS: usize = 16;
const EXIT_OK: i32 = 0;
const EXIT_CRYPTO: i32 = 2;
const EXIT_POLICY: i32 = 3;
const EXIT_IO: i32 = 4;

use quorum::{
    load_intent, make_fragment, quorum_add, quorum_init, quorum_seal, save_fragment,
    validate_required_signatures, Intent,
};

/// Quantum‑Sign — Post-quantum code-signing CLI (secure defaults)
#[derive(Parser, Debug)]
#[command(name = "quantum-sign", version, about)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Generate a signing keypair (software-only).
    Keygen {
        /// Profile (supports only mldsa-87 for now)
        #[arg(long, default_value = "mldsa-87")]
        profile: String,
        /// Output secret-key path
        #[arg(long)]
        secret: Utf8PathBuf,
        /// Output public-key path
        /// Corresponding verifying key path
        #[arg(long)]
        public: Utf8PathBuf,
    },
    /// Prepare a quorum intent for signing.
    Sign {
        /// Input artifact to be signed
        #[arg(long)]
        inp: Utf8PathBuf,
        /// Future .qsig output path (used to derive intent path)
        #[arg(long)]
        sig: Utf8PathBuf,
        /// Digest algorithm (`sha256`, `sha512`, or `shake256-64`; Level-5 defaults to `sha512`)
        #[arg(long, default_value = "sha512")]
        digest: String,
        /// Policy file controlling the signing (JSON by default)
        #[arg(long)]
        policy: Option<Utf8PathBuf>,
        /// Allow non-FIPS algorithms (defaults to false)
        #[arg(long, default_value_t = false)]
        allow_nonfips: bool,
        /// Allow running without a policy file
        #[arg(long, default_value_t = false)]
        allow_missing_policy: bool,
        /// Allow unsafe path patterns (parent dirs, symlinks)
        #[arg(long, default_value_t = false)]
        allow_unsafe_inputs: bool,
        /// Restrict allowed signer key IDs (repeatable)
        #[arg(long = "allowed-kid")]
        allowed_kid: Vec<String>,
    },
    /// Verify a .qsig file offline (placeholder)
    Verify {
        /// Input artifact
        #[arg(long)]
        inp: Utf8PathBuf,
        /// Signature file
        #[arg(long)]
        sig: Utf8PathBuf,
        /// Trust database path
        #[arg(long)]
        trustdb: Option<Utf8PathBuf>,
        /// Emit JSON report instead of plain text
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Attach an RFC3161 timestamp (explicit; not in sign hotpath)
    Timestamp {
        /// Signature file to update
        #[arg(long)]
        sig: Utf8PathBuf,
        /// TSA URL
        #[arg(long)]
        tsa: String,
    },
    /// Submit or verify transparency proofs (explicit)
    Log {
        /// Signature file
        #[arg(long)]
        sig: Utf8PathBuf,
        /// Transparency log URL
        #[arg(long)]
        log_url: String,
    },
    /// Manage trust store entries
    Trust {
        #[command(subcommand)]
        action: TrustAction,
    },
    /// Manage quorum signing lifecycle
    Quorum {
        #[command(subcommand)]
        action: QuorumAction,
    },
    /// Print CLI reference (manpage-style)
    Man,
}

#[derive(Subcommand, Debug)]
enum QuorumAction {
    /// Initialise an intent/journal pair manually
    Init {
        #[arg(long)]
        artifact: Utf8PathBuf,
        #[arg(long)]
        policy: Utf8PathBuf,
        /// Digest algorithm (`sha256`, `sha512`, or `shake256-64`; Level-5 defaults to `sha512`)
        #[arg(long, default_value = "sha512")]
        digest: String,
        /// Intent output path (defaults to `<artifact>.qsi`)
        #[arg(long)]
        intent: Option<Utf8PathBuf>,
        /// Restrict allowed key identifiers (repeatable)
        #[arg(long = "allowed-kid")]
        allowed_kid: Vec<String>,
    },
    /// Add a cosign fragment to the journal
    Add {
        #[arg(long)]
        part: Utf8PathBuf,
        /// Output fragment path
        #[arg(long)]
        fragment: Utf8PathBuf,
    },
    /// Produce a cosign fragment for an intent
    Cosign {
        #[arg(long)]
        intent: Utf8PathBuf,
        #[arg(long)]
        secret: Utf8PathBuf,
        /// Corresponding verifying key path
        #[arg(long)]
        public: Utf8PathBuf,
        /// Output fragment path
        #[arg(long)]
        fragment: Utf8PathBuf,
        /// Optionally append the new fragment to a journal (`.qsig.part`).
        #[arg(long)]
        append: Option<Utf8PathBuf>,
    },
    /// Seal a journal into a final `.qsig` using a trust store of verifying keys
    Seal {
        #[arg(long)]
        part: Utf8PathBuf,
        #[arg(long)]
        out: Utf8PathBuf,
        #[arg(long)]
        trust_dir: Utf8PathBuf,
    },
    /// One-shot: produce minimal release (png, qsig, policy.json, trust/KID.spki) and optional zip
    Package {
        /// Input artifact to sign
        #[arg(long)]
        artifact: Utf8PathBuf,
        /// Policy file (JSON/YAML)
        #[arg(long)]
        policy: Utf8PathBuf,
        /// Digest algorithm
        #[arg(long, default_value = "sha512")]
        digest: String,
        /// Existing secret key (DER or raw) to sign with (optional)
        #[arg(long)]
        secret: Option<Utf8PathBuf>,
        /// Existing public key (raw verifying key). If omitted and secret missing, a fresh keypair is generated in-memory.
        #[arg(long)]
        public: Option<Utf8PathBuf>,
        /// Output directory for the release package
        #[arg(long, default_value = "release")]
        out_dir: Utf8PathBuf,
        /// Also build a zip next to out_dir (best-effort)
        #[arg(long, default_value_t = true)]
        zip: bool,
    },
}

#[derive(Subcommand, Debug)]
enum TrustAction {
    /// Import a public key (PEM or DER) into the trust store
    Import {
        #[arg(long)]
        public: Utf8PathBuf,
        #[arg(long, default_value = "./trust")]
        trustdb: Utf8PathBuf,
    },
    /// List trust store entries (KID filenames)
    List {
        #[arg(long, default_value = "./trust")]
        trustdb: Utf8PathBuf,
    },
}

fn main() {
    let code = match run() {
        Ok(()) => EXIT_OK,
        Err(err) => {
            eprintln!("error: {err}");
            match err {
                CliError::Crypto(_) => EXIT_CRYPTO,
                CliError::Io(_) => EXIT_IO,
                CliError::PolicyRequired
                | CliError::PolicyLoad(_)
                | CliError::PolicyValidation(_)
                | CliError::UnsupportedDigest(_)
                | CliError::InvalidArgument(_)
                | CliError::UnsafePath(_)
                | CliError::Other(_) => EXIT_POLICY,
            }
        }
    };
    process::exit(code);
}

fn run() -> Result<(), CliError> {
    let cli = Cli::parse();
    match cli.command {
        Commands::Man => {
            const CLI_MAN: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/CLI.md"));
            println!("{}", CLI_MAN);
            Ok(())
        }
        Commands::Keygen {
            profile,
            secret,
            public,
        } => handle_keygen(&profile, secret, public),
        Commands::Sign {
            inp,
            sig,
            digest,
            policy,
            allow_nonfips,
            allow_missing_policy,
            allow_unsafe_inputs,
            allowed_kid,
        } => handle_sign(
            inp,
            sig,
            digest,
            policy,
            allow_nonfips,
            allow_missing_policy,
            allow_unsafe_inputs,
            allowed_kid,
        ),
        Commands::Verify {
            inp,
            sig,
            trustdb,
            json,
        } => handle_verify(inp, sig, trustdb, json),
        Commands::Timestamp { sig, tsa } => {
            println!("(stub) timestamp --sig {sig} --tsa {tsa}");
            Ok(())
        }
        Commands::Log { sig, log_url } => {
            println!("(stub) log --sig {sig} --log-url {log_url}");
            Ok(())
        }
        Commands::Trust { action } => handle_trust(action),
        Commands::Quorum { action } => handle_quorum(action),
    }
}

fn handle_keygen(profile: &str, secret: Utf8PathBuf, public: Utf8PathBuf) -> Result<(), CliError> {
    if profile != "mldsa-87" {
        return Err(CliError::InvalidArgument("only mldsa-87 is supported"));
    }

    ensure_output_parent(&secret)?;
    ensure_output_parent(&public)?;

    let mut drbg = HmacSha512Drbg::from_os(Some(b"qs-keygen"))?;
    let keypair = qs_crypto::keypair_mldsa87(&mut drbg)?;

    let mut sk_file = create_exclusive(&secret)?;
    sk_file.write_all(&keypair.secret)?;

    let mut pk_file = create_exclusive(&public)?;
    pk_file.write_all(&keypair.public)?;

    let kid = qs_crypto::kid_from_public_key(&keypair.public)?;
    println!("generated mldsa-87 keypair\n  secret: {secret}\n  public: {public}\n  kid: {kid}");
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn handle_sign(
    artifact: Utf8PathBuf,
    sig_out: Utf8PathBuf,
    digest_alg: String,
    policy_path: Option<Utf8PathBuf>,
    allow_nonfips: bool,
    allow_missing_policy: bool,
    allow_unsafe_inputs: bool,
    allowed_kids: Vec<String>,
) -> Result<(), CliError> {
    if !allow_unsafe_inputs {
        validate_path(&artifact)?;
        ensure_output_parent(&sig_out)?;
    }

    let digest_alg = parse_digest_alg(&digest_alg)?;
    let policy = load_policy(policy_path, allow_missing_policy)?;

    if let Some(pol) = &policy {
        pol.ensure_fips(allow_nonfips)?;
        pol.enforce_level5()?;
    }

    let policy = match policy {
        Some(p) => p,
        None => return Err(CliError::PolicyRequired),
    };

    let policy_digest_alg = DigestAlg::from_str(&policy.digest_alg).map_err(|_| {
        CliError::Other(format!(
            "unsupported digest_alg '{}' in policy",
            policy.digest_alg
        ))
    })?;

    if policy.require_level5 && !policy.allow_lower_levels && digest_alg != policy_digest_alg {
        return Err(CliError::Other(format!(
            "digest '{}' does not match policy Level-5 requirement '{}'",
            digest_alg.as_str(),
            policy.digest_alg
        )));
    }

    let digest = compute_digest(&artifact, digest_alg)?;

    let intent_path = sig_out.with_extension("qsi");
    let allowed = dedup_allowed(allowed_kids);
    if let Some(req) = policy.required_signatures {
        validate_required_signatures(&req)
            .map_err(|_| CliError::InvalidArgument("invalid required_signatures in policy"))?;
        if req.m > 1 && allowed.is_empty() {
            return Err(CliError::InvalidArgument(
                "multi-signature policy requires --allowed-kid entries",
            ));
        }
        if !allowed.is_empty() && allowed.len() < req.m as usize {
            return Err(CliError::InvalidArgument(
                "not enough allowed kids to satisfy quorum",
            ));
        }
        if !allowed.is_empty() && allowed.len() > req.n as usize {
            return Err(CliError::InvalidArgument("allowed kid list exceeds quorum"));
        }
    }

    quorum_init(
        &intent_path,
        &policy,
        &policy.default_alg,
        &digest,
        digest_alg.as_str(),
        &allowed,
    )?;

    println!(
        "prepared intent {intent_path} and journal {} (final `.qsig` will be emitted after quorum seal)",
        intent_path.with_extension("qsig.part")
    );
    Ok(())
}

fn handle_trust(action: TrustAction) -> Result<(), CliError> {
    match action {
        TrustAction::Import { public, trustdb } => {
            validate_path(&public)?;
            validate_path(&trustdb)?;
            let raw = fs::read(public.as_std_path())?;
            let der = match qs_crypto::public::spki_der_canonical(&raw) {
                Ok(der) => der,
                Err(err) => qs_crypto::public_key_to_spki(&raw)
                    .map_err(|_| CliError::Other(err.to_string()))?,
            };
            let param =
                qs_crypto::spki_mldsa_paramset(&der).map_err(|e| CliError::Other(e.to_string()))?;
            if param != "mldsa-87" {
                return Err(CliError::Other(format!(
                    "trust import rejected: {param} is not ML-DSA-87",
                )));
            }
            let kid = qs_crypto::public::kid_from_spki_der(&der);

            fs::create_dir_all(trustdb.as_std_path())?;
            let out = trustdb.join(format!("{kid}.spki"));
            quorum::write_atomic_sync(&out, &der)?;
            println!("imported {kid} -> {out}");
            Ok(())
        }
        TrustAction::List { trustdb } => {
            if !trustdb.exists() {
                return Ok(());
            }
            validate_path(&trustdb)?;

            let mut entries = vec![];
            for entry in fs::read_dir(trustdb.as_std_path())? {
                let entry = entry?;
                if !entry.file_type()?.is_file() {
                    continue;
                }
                let path = entry.path();
                let Ok(path_utf8) = Utf8PathBuf::from_path_buf(path) else {
                    continue;
                };
                let Some(name) = path_utf8.file_name() else {
                    continue;
                };

                let (stem, der) = if let Some(stem) = name.strip_suffix(".spki") {
                    let data = fs::read(path_utf8.as_std_path())?;
                    let der = qs_crypto::public::spki_der_canonical(&data)
                        .map_err(|e| CliError::Other(e.to_string()))?;
                    (stem.to_string(), der)
                } else if let Some(stem) = name.strip_suffix(".vk") {
                    let data = fs::read(path_utf8.as_std_path())?;
                    let der = qs_crypto::public_key_to_spki(&data)?;
                    (stem.to_string(), der)
                } else {
                    continue;
                };

                let param = qs_crypto::spki_mldsa_paramset(&der)
                    .map_err(|e| CliError::Other(e.to_string()))?;
                if param != "mldsa-87" {
                    return Err(CliError::Other(format!(
                        "trust entry {path_utf8} is {param}, not ML-DSA-87",
                    )));
                }

                let kid = qs_crypto::kid_from_spki_der(&der);
                if kid != stem {
                    return Err(CliError::Other(format!(
                        "trust entry KID mismatch: {}",
                        path_utf8
                    )));
                }
                entries.push((kid, path_utf8));
            }
            entries.sort_by(|a, b| a.0.cmp(&b.0));
            for (kid, path) in entries {
                println!("{kid} {path}");
            }
            Ok(())
        }
    }
}

fn handle_quorum(action: QuorumAction) -> Result<(), CliError> {
    match action {
        QuorumAction::Init {
            artifact,
            policy,
            digest,
            intent,
            allowed_kid,
        } => {
            let digest_alg = parse_digest_alg(&digest)?;
            let digest_bytes = compute_digest(&artifact, digest_alg)?;
            let policy =
                qs_policy::load_policy_file(policy.as_std_path()).map_err(CliError::PolicyLoad)?;
            policy.ensure_fips(false)?;
            policy.enforce_level5()?;
            let policy_digest_alg = DigestAlg::from_str(&policy.digest_alg).map_err(|_| {
                CliError::Other(format!(
                    "unsupported digest_alg '{}' in policy",
                    policy.digest_alg
                ))
            })?;
            if policy.require_level5
                && !policy.allow_lower_levels
                && digest_alg != policy_digest_alg
            {
                return Err(CliError::Other(format!(
                    "digest '{}' does not match policy Level-5 requirement '{}'",
                    digest_alg.as_str(),
                    policy.digest_alg
                )));
            }
            let intent_path = intent.unwrap_or_else(|| artifact.with_extension("qsi"));
            let allowed = dedup_allowed(allowed_kid);
            quorum_init(
                &intent_path,
                &policy,
                &policy.default_alg,
                &digest_bytes,
                digest_alg.as_str(),
                &allowed,
            )?;
            println!(
                "initialized quorum intent at {intent_path} with journal {}",
                intent_path.with_extension("qsig.part")
            );
            Ok(())
        }
        QuorumAction::Add { part, fragment } => {
            quorum_add(&part, &fragment)?;
            println!("added fragment {fragment} to journal {part}");
            Ok(())
        }
        QuorumAction::Cosign {
            intent,
            secret,
            public,
            fragment,
            append,
        } => {
            let intent_obj: Intent = load_intent(&intent)?;
            if intent_obj.v != 1 {
                return Err(CliError::InvalidArgument("unsupported intent version"));
            }
            validate_path(&secret)?;
            validate_path(&public)?;

            let pk_bytes = fs::read(public.as_std_path())?;
            let derived_kid = qs_crypto::kid_from_public_key(&pk_bytes)?;
            if !intent_obj.allowed_kids.is_empty()
                && !intent_obj.allowed_kids.contains(&derived_kid)
            {
                return Err(CliError::InvalidArgument("kid not permitted by intent"));
            }

            let sk_bytes = fs::read(secret.as_std_path())?;

            let mut drbg = HmacSha512Drbg::from_os(Some(b"qs-cosign"))?;
            let digest_alg = DigestAlg::from_str(&intent_obj.digest_alg)
                .map_err(|_| CliError::Other("intent digest_alg unsupported".into()))?;
            let sig = qs_crypto::sign_mldsa87(
                &mut drbg,
                &sk_bytes,
                intent_obj.digest.as_ref(),
                digest_alg,
                Some(intent_obj.policy_hash.as_ref()),
            )?;
            qs_crypto::verify_mldsa87(
                &pk_bytes,
                intent_obj.digest.as_ref(),
                digest_alg,
                &sig,
                Some(intent_obj.policy_hash.as_ref()),
            )?;
            let fragment_obj = make_fragment(&intent_obj, &derived_kid, sig);
            save_fragment(&fragment, &fragment_obj)?;
            println!("wrote fragment {fragment}");
            if let Some(part) = append {
                validate_path(&part)?;
                quorum_add(&part, &fragment)?;
                println!("appended fragment into {part}");
            }
            Ok(())
        }
        QuorumAction::Seal {
            part,
            out,
            trust_dir,
        } => {
            let verify = move |kid: &str,
                               alg: &str,
                               digest: &[u8],
                               digest_alg: DigestAlg,
                               sig: &[u8],
                               policy: &[u8]|
                  -> bool {
                if alg != "mldsa-87" {
                    return false;
                }
                let spki_path = trust_dir.join(format!("{kid}.spki"));
                let spki_bytes = match fs::read(spki_path.as_std_path()) {
                    Ok(bytes) => bytes,
                    Err(_) => {
                        let legacy = trust_dir.join(format!("{kid}.vk"));
                        match fs::read(legacy.as_std_path()) {
                            Ok(vk_bytes) => match qs_crypto::public_key_to_spki(&vk_bytes) {
                                Ok(spki) => spki,
                                Err(_) => return false,
                            },
                            Err(_) => return false,
                        }
                    }
                };
                if qs_crypto::kid_from_spki_der(&spki_bytes) != kid {
                    return false;
                }
                qs_crypto::verify_mldsa87_spki(&spki_bytes, digest, digest_alg, sig, Some(policy))
                    .is_ok()
            };
            quorum_seal(&part, &out, verify)?;
            println!("sealed journal {part} into {out}");
            Ok(())
        }
        QuorumAction::Package {
            artifact,
            policy,
            digest,
            secret,
            public,
            out_dir,
            zip,
        } => {
            validate_path(&artifact)?;
            let policy = qs_policy::load_policy_file(policy.as_std_path()).map_err(CliError::PolicyLoad)?;
            policy.ensure_fips(false)?; policy.enforce_level5()?;
            let digest_alg = parse_digest_alg(&digest)?;
            let digest_bytes = compute_digest(&artifact, digest_alg)?;

            // Determine keys: use provided or generate in-memory
            let (sk_bytes, vk_spki, kid): (Vec<u8>, Vec<u8>, String) = if let (Some(sk), Some(pk)) = (secret.as_ref(), public.as_ref()) {
                let sk_b = fs::read(sk.as_std_path())?;
                let pk_b = fs::read(pk.as_std_path())?;
                let spki = qs_crypto::public_key_to_spki(&pk_b)?;
                let kid = qs_crypto::kid_from_spki_der(&spki);
                (sk_b, spki, kid)
            } else {
                let mut drbg = HmacSha512Drbg::from_os(Some(b"qs-package"))?;
                let kp = qs_crypto::keypair_mldsa87(&mut drbg)?;
                let spki = qs_crypto::public_key_to_spki(&kp.public)?;
                let kid = qs_crypto::kid_from_spki_der(&spki);
                (kp.secret, spki, kid)
            };

            // Temp intent/journal
            let dir = tempfile::tempdir().map_err(|e| CliError::Other(format!("tempdir: {e}")))?;
            let intent_path = Utf8PathBuf::from_path_buf(dir.path().join("intent.qsi")).unwrap();
            let allowed = vec![kid.clone()];
            quorum_init(
                &intent_path,
                &policy,
                &policy.default_alg,
                &digest_bytes,
                digest_alg.as_str(),
                &allowed,
            )?;
            let it: Intent = load_intent(&intent_path)?;
            let mut drbg2 = HmacSha512Drbg::from_os(Some(b"qs-package-cs")).map_err(|e| CliError::Crypto(e.into()))?;
            let da = DigestAlg::from_str(&it.digest_alg).map_err(|_| CliError::Other("digest".into()))?;
            let sig = qs_crypto::sign_mldsa87(&mut drbg2, &sk_bytes, it.digest.as_ref(), da, Some(it.policy_hash.as_ref()))?;
            let frag = make_fragment(&it, &kid, sig);
            let frag_path = Utf8PathBuf::from_path_buf(dir.path().join("frag.csf")).unwrap();
            save_fragment(&frag_path, &frag)?;
            let part_path = intent_path.with_extension("qsig.part");
            quorum_add(&part_path, &frag_path)?;

            // Seal final signature into out_dir/SIG
            fs::create_dir_all(out_dir.as_std_path())?;
            let sig_out = out_dir.join(artifact.file_name().unwrap_or("artifact")).with_extension("qsig");
            let verify = |kidv: &str, alg: &str, digest: &[u8], da2: DigestAlg, sigb: &[u8], policy: &[u8]| -> bool {
                if alg != "mldsa-87" { return false; }
                if kidv != kid { return false; }
                qs_crypto::verify_mldsa87_spki(&vk_spki, digest, da2, sigb, Some(policy)).is_ok()
            };
            quorum_seal(&part_path, &sig_out, verify)?;

            // Copy artifact, policy, trust
            let img_out = out_dir.join(artifact.file_name().unwrap_or("artifact"));
            fs::copy(artifact.as_std_path(), img_out.as_std_path())?;
            let pol_out = out_dir.join("policy.json");
            let pol_json = serde_json::to_vec_pretty(&policy).map_err(|e| CliError::Other(format!("policy serialize: {e}")))?;
            fs::write(pol_out.as_std_path(), pol_json)?;
            let trust_dir = out_dir.join("trust");
            fs::create_dir_all(trust_dir.as_std_path())?;
            let spki_out = trust_dir.join(format!("{}.spki", kid));
            fs::write(spki_out.as_std_path(), &vk_spki)?;

            // Optional zip (-X equivalent is platform tool; best-effort plain zip)
            if zip {
                let zip_path = out_dir.with_extension("zip");
                // Try system zip; if not present, skip silently
                let cmd = std::process::Command::new("zip")
                    .current_dir(out_dir.as_std_path())
                    .args(["-r", "-X", zip_path.as_std_path().to_str().unwrap_or("../package.zip"), "."]) // -X ignored if not supported
                    .output();
                let _ = cmd;
            }

            println!("packaged minimal release in {}", out_dir);
            Ok(())
        }
    }
}


fn handle_verify(
    inp: Utf8PathBuf,
    sig: Utf8PathBuf,
    trustdb: Option<Utf8PathBuf>,
    json: bool,
) -> Result<(), CliError> {
    validate_path(&inp)?;
    validate_path(&sig)?;

    let trust_dir = trustdb.unwrap_or_else(|| Utf8PathBuf::from("./trust"));
    validate_path(&trust_dir)?;
    let trust_map = load_trust_store(&trust_dir)?;

    let sig_bytes = fs::read(sig.as_std_path())?;
    if sig_bytes.len() > 256 * 1024 {
        return Err(CliError::Other("qsig too large".into()));
    }
    let qsig = QSig::decode(&sig_bytes)
        .map_err(|e| CliError::Other(format!("qsig decode failed: {e}")))?;
    let mut reencoded = Vec::new();
    ciborium::ser::into_writer(&qsig, &mut reencoded)
        .map_err(|e| CliError::Other(format!("qsig re-encode: {e}")))?;
    let canonical_ok = reencoded == sig_bytes;
    if !canonical_ok {
        return Err(CliError::Other(
            "non-canonical .qsig encoding (re-encode mismatch)".into(),
        ));
    }

    if qsig.version != 1 {
        return Err(CliError::Other("unsupported qsig version".into()));
    }
    if qsig.alg != "mldsa-87" {
        return Err(CliError::Other(format!(
            "unsupported algorithm '{}'",
            qsig.alg
        )));
    }
    if qsig.domain_sep != "quantum-sign-v1" {
        return Err(CliError::Other("unexpected domain separator".into()));
    }

    let claims = parse_claims(&qsig.signer.claims)?;
    claims.validate()?;

    if claims.n as usize > MAX_SIGNERS {
        return Err(CliError::Other(
            "claims.n exceeds maximum supported signers (16)".into(),
        ));
    }

    let total_sigs = 1 + qsig.co_sig.len();
    if total_sigs > claims.n as usize {
        return Err(CliError::Other("too many signatures for claims.n".into()));
    }
    if total_sigs < claims.m as usize {
        return Err(CliError::Other(
            "embedded signatures fewer than claims.m".into(),
        ));
    }

    let policy_meta = qsig
        .meta
        .as_ref()
        .map(|b| b.as_ref())
        .ok_or_else(|| CliError::Other("signature missing policy hash".into()))?;
    if policy_meta != claims.policy_hash.as_slice() {
        return Err(CliError::Other("policy hash mismatch".into()));
    }

    let digest_alg = match claims.digest_alg.as_str() {
        "sha512" => DigestAlg::Sha512,
        "shake256-64" => DigestAlg::Shake256_64,
        other => {
            return Err(CliError::Other(format!(
                "unsupported digest in claims: {other}"
            )))
        }
    };

    let artifact_digest = compute_digest(&inp, digest_alg)?;
    if artifact_digest.as_slice() != qsig.digest.as_slice() {
        return Err(CliError::Other("message digest mismatch".into()));
    }

    let mut used = vec![false; claims.kids.len()];
    let mut valid = 0usize;
    let mut verified_kids = Vec::new();

    let mut match_signature = |sig_bytes: &[u8]| -> Result<Option<String>, CliError> {
        for (idx, kid) in claims.kids.iter().enumerate() {
            if used[idx] {
                continue;
            }
            let spki = trust_map
                .get(kid)
                .ok_or_else(|| CliError::Other(format!("trust entry not found for kid {kid}")))?;
            if qs_crypto::verify_mldsa87_spki(
                spki,
                qsig.digest.as_slice(),
                digest_alg,
                sig_bytes,
                Some(claims.policy_hash.as_slice()),
            )
            .is_ok()
            {
                used[idx] = true;
                return Ok(Some(kid.clone()));
            }
        }
        Ok(None)
    };

    if let Some(kid) = match_signature(&qsig.sig)? {
        valid += 1;
        verified_kids.push(kid);
    }
    for cs in &qsig.co_sig {
        if let Some(kid) = match_signature(cs.as_ref())? {
            valid += 1;
            verified_kids.push(kid);
        }
    }

    if valid < claims.m as usize {
        return Err(CliError::Other(format!(
            "verified {valid} signatures but policy requires {}",
            claims.m
        )));
    }

    if json {
        verified_kids.sort();
        let report = VerifyReport {
            status: "ok",
            alg: &qsig.alg,
            digest_alg: claims.digest_alg.clone(),
            domain: qsig.domain_sep.clone(),
            canonical: canonical_ok,
            m: claims.m,
            n: claims.n,
            kids_total: claims.kids.len(),
            kids_verified: verified_kids.len(),
            verified_kids,
            policy_hash_hex: to_hex(&claims.policy_hash),
            file_digest_hex: to_hex(&artifact_digest),
            qsig_size: sig_bytes.len(),
            message: None,
        };
        let json_out = serde_json::to_string_pretty(&report)
            .map_err(|e| CliError::Other(format!("json encode failed: {e}")))?;
        println!("{json_out}");
    } else {
        println!(
            "OK: verified {}/{} unique signatures (Level-5, {})",
            verified_kids.len(),
            claims.n,
            claims.digest_alg
        );
    }
    Ok(())
}

struct ClaimsTuple {
    policy_hash: Vec<u8>,
    kids: Vec<String>,
    m: u64,
    n: u64,
    digest_alg: String,
}

impl ClaimsTuple {
    fn validate(&self) -> Result<(), CliError> {
        if self.m == 0 || self.n == 0 || self.m > self.n {
            return Err(CliError::Other("invalid m-of-n in claims".into()));
        }
        if self.kids.len() < self.m as usize {
            return Err(CliError::Other("claims.kids shorter than claims.m".into()));
        }
        if self.kids.len() > self.n as usize {
            return Err(CliError::Other("claims.kids exceeds claims.n".into()));
        }
        if self.kids.windows(2).any(|w| w[0].as_str() >= w[1].as_str()) {
            return Err(CliError::Other(
                "claims.kids must be strictly sorted and unique".into(),
            ));
        }
        Ok(())
    }
}

#[derive(Serialize)]
struct VerifyReport<'a> {
    status: &'a str,
    alg: &'a str,
    digest_alg: String,
    domain: String,
    canonical: bool,
    m: u64,
    n: u64,
    kids_total: usize,
    kids_verified: usize,
    verified_kids: Vec<String>,
    policy_hash_hex: String,
    file_digest_hex: String,
    qsig_size: usize,
    message: Option<String>,
}

fn parse_claims(bytes: &[u8]) -> Result<ClaimsTuple, CliError> {
    let raw: (serde_bytes::ByteBuf, Vec<String>, u64, u64, String) =
        ciborium::de::from_reader(bytes)
            .map_err(|e| CliError::Other(format!("claims decode failed: {e}")))?;
    Ok(ClaimsTuple {
        policy_hash: raw.0.into_vec(),
        kids: raw.1,
        m: raw.2,
        n: raw.3,
        digest_alg: raw.4,
    })
}

fn to_hex(bytes: &[u8]) -> String {
    let mut out = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        let _ = write!(&mut out, "{:02x}", byte);
    }
    out
}

fn load_trust_store(dir: &Utf8Path) -> Result<BTreeMap<String, Vec<u8>>, CliError> {
    let mut map = BTreeMap::new();
    if !dir.exists() {
        return Ok(map);
    }

    for entry in fs::read_dir(dir.as_std_path())? {
        let entry = entry?;
        if !entry.file_type()?.is_file() {
            continue;
        }
        let path = entry.path();
        let Ok(path_utf8) = Utf8PathBuf::from_path_buf(path) else {
            continue;
        };
        let Some(name) = path_utf8.file_name() else {
            continue;
        };

        let (stem, der) = if let Some(stem) = name.strip_suffix(".spki") {
            let data = fs::read(path_utf8.as_std_path())?;
            let der = qs_crypto::public::spki_der_canonical(&data)
                .map_err(|e| CliError::Other(e.to_string()))?;
            (stem.to_string(), der)
        } else if let Some(stem) = name.strip_suffix(".vk") {
            let data = fs::read(path_utf8.as_std_path())?;
            let der = qs_crypto::public_key_to_spki(&data)?;
            (stem.to_string(), der)
        } else {
            continue;
        };

        let kid = qs_crypto::kid_from_spki_der(&der);
        if kid != stem {
            return Err(CliError::Other(format!(
                "trust entry KID mismatch: {}",
                path_utf8
            )));
        }
        map.entry(kid).or_insert(der);
    }

    Ok(map)
}

fn parse_digest_alg(flag: &str) -> Result<DigestAlg, CliError> {
    DigestAlg::from_str(flag).map_err(|_| CliError::UnsupportedDigest(flag.to_string()))
}

fn compute_digest(path: &Utf8Path, alg: DigestAlg) -> Result<Vec<u8>, CliError> {
    use std::fs::File;
    use std::io::{BufReader, Read};

    let file = File::open(path.as_std_path())?;
    let mut reader = BufReader::new(file);
    let mut buf = [0u8; 64 * 1024];

    let digest = match alg {
        DigestAlg::Sha256 => {
            let mut hasher = Sha256::new();
            loop {
                let n = reader.read(&mut buf)?;
                if n == 0 {
                    break;
                }
                Digest::update(&mut hasher, &buf[..n]);
            }
            hasher.finalize().to_vec()
        }
        DigestAlg::Sha512 => {
            let mut hasher = Sha512::new();
            loop {
                let n = reader.read(&mut buf)?;
                if n == 0 {
                    break;
                }
                Digest::update(&mut hasher, &buf[..n]);
            }
            hasher.finalize().to_vec()
        }
        DigestAlg::Shake256_64 => {
            let mut hasher = Shake256::default();
            loop {
                let n = reader.read(&mut buf)?;
                if n == 0 {
                    break;
                }
                sha3::digest::Update::update(&mut hasher, &buf[..n]);
            }
            let mut reader_xof = hasher.finalize_xof();
            let mut out = vec![0u8; alg.output_len()];
            XofReader::read(&mut reader_xof, &mut out);
            out
        }
    };
    Ok(digest)
}

fn dedup_allowed(mut allowed: Vec<String>) -> Vec<String> {
    allowed.sort();
    allowed.dedup();
    allowed
}

fn load_policy(path: Option<Utf8PathBuf>, allow_missing: bool) -> Result<Option<Policy>, CliError> {
    match (path, allow_missing) {
        (Some(path), _) => Ok(Some(
            qs_policy::load_policy_file(path.as_std_path()).map_err(CliError::PolicyLoad)?,
        )),
        (None, true) => Ok(None),
        (None, false) => Err(CliError::PolicyRequired),
    }
}

mod io_safe {
    use super::CliError;
    use camino::{Utf8Component, Utf8Path};
    use std::fs::File;

    #[cfg(unix)]
    use std::os::unix::fs::OpenOptionsExt;

    pub fn validate_path(path: &Utf8Path) -> Result<(), CliError> {
        if path
            .components()
            .any(|c| matches!(c, Utf8Component::ParentDir))
        {
            return Err(CliError::UnsafePath(path.to_path_buf()));
        }
        Ok(())
    }

    pub fn create_exclusive(path: &Utf8Path) -> Result<File, CliError> {
        #[cfg(unix)]
        {
            let mut opts = std::fs::OpenOptions::new();
            opts.write(true)
                .create_new(true)
                .mode(0o600)
                .custom_flags(libc::O_NOFOLLOW);
            opts.open(path).map_err(CliError::from)
        }
        #[cfg(not(unix))]
        {
            std::fs::OpenOptions::new()
                .write(true)
                .create_new(true)
                .open(path)
                .map_err(CliError::from)
        }
    }

    pub fn ensure_output_parent(path: &Utf8Path) -> Result<(), CliError> {
        if let Some(parent) = path.parent() {
            validate_path(parent)?;
        }
        Ok(())
    }
}

use io_safe::{create_exclusive, ensure_output_parent, validate_path};

#[derive(Debug)]
enum CliError {
    PolicyRequired,
    PolicyLoad(qs_policy::Error),
    PolicyValidation(ValidationError),
    UnsupportedDigest(String),
    InvalidArgument(&'static str),
    UnsafePath(Utf8PathBuf),
    Crypto(qs_crypto::CryptoError),
    Io(io::Error),
    Other(String),
}

impl fmt::Display for CliError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CliError::PolicyRequired => write!(
                f,
                "policy file required (use --policy or --allow-missing-policy)"
            ),
            CliError::PolicyLoad(err) => write!(f, "policy load failed: {err:?}"),
            CliError::PolicyValidation(err) => write!(f, "policy validation failed: {err:?}"),
            CliError::UnsupportedDigest(d) => write!(f, "unsupported digest algorithm: {d}"),
            CliError::InvalidArgument(msg) => write!(f, "invalid argument: {msg}"),
            CliError::UnsafePath(path) => write!(f, "unsafe path rejected: {path}"),
            CliError::Crypto(err) => write!(f, "crypto error: {err}"),
            CliError::Io(err) => write!(f, "I/O error: {err}"),
            CliError::Other(msg) => write!(f, "{msg}"),
        }
    }
}

impl StdError for CliError {}

impl From<qs_policy::Error> for CliError {
    fn from(err: qs_policy::Error) -> Self {
        CliError::PolicyLoad(err)
    }
}

impl From<ValidationError> for CliError {
    fn from(err: ValidationError) -> Self {
        CliError::PolicyValidation(err)
    }
}

impl From<qs_crypto::CryptoError> for CliError {
    fn from(err: qs_crypto::CryptoError) -> Self {
        CliError::Crypto(err)
    }
}

impl From<qs_crypto::DrbgError> for CliError {
    fn from(err: qs_crypto::DrbgError) -> Self {
        CliError::Crypto(qs_crypto::CryptoError::from(err))
    }
}

impl From<io::Error> for CliError {
    fn from(err: io::Error) -> Self {
        CliError::Io(err)
    }
}
