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

use ant_protocol::pqc::api::{ml_dsa_65, MlDsaPublicKey, MlDsaSignature, MlDsaVariant};
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};

use crate::error::{Error, Result};
use crate::node::binary::{extract_tar_gz, extract_zip, ProgressReporter};

const GITHUB_REPO: &str = "WithAutonomi/ant-client";
const CLI_BINARY_NAME: &str = "ant";
const TAG_PREFIX: &str = "ant-cli-v";

/// Signing context for domain separation (must match the context used by `ant-keygen sign`).
const SIGNING_CONTEXT: &[u8] = b"ant-release-v1";

/// ML-DSA-65 signature size in bytes.
const SIGNATURE_SIZE: usize = 3309;

/// Embedded release signing public key (ML-DSA-65).
///
/// This key is used to verify signatures on released ant-client binaries.
/// The corresponding private key is held by authorised release signers.
/// Generated: 2026-03-30 19:49:36 UTC
const RELEASE_SIGNING_KEY: &[u8] = &[
    0xb4, 0xcf, 0x2a, 0x24, 0x31, 0xd9, 0xb2, 0x3a, 0xab, 0xe9, 0x5e, 0xfc, 0xbc, 0xf3, 0xb1, 0x1f,
    0x4e, 0x50, 0x0a, 0x46, 0xeb, 0x83, 0xfc, 0x6f, 0x0f, 0x89, 0x41, 0x00, 0x1b, 0x52, 0xde, 0xdc,
    0xb5, 0xc9, 0x07, 0xed, 0x72, 0x3a, 0xe1, 0xa9, 0x82, 0xe1, 0xfc, 0xff, 0xab, 0xce, 0x9f, 0x7d,
    0xab, 0xe2, 0x57, 0x92, 0xe0, 0xf2, 0xac, 0xa1, 0x41, 0xd3, 0x74, 0x95, 0x41, 0xd1, 0xac, 0x22,
    0xb1, 0xbb, 0x5c, 0xf4, 0x02, 0x0b, 0x73, 0x85, 0xfd, 0x56, 0x75, 0x0d, 0x5c, 0x38, 0xe1, 0x2d,
    0xe0, 0x15, 0x4f, 0xbf, 0x40, 0xeb, 0xf1, 0x0d, 0x8f, 0x39, 0x32, 0xeb, 0x80, 0xa7, 0x2e, 0x77,
    0x3e, 0x54, 0xe3, 0x3d, 0x62, 0xae, 0xe7, 0x09, 0x5e, 0xfb, 0xdc, 0xaa, 0x07, 0xdc, 0xe1, 0x08,
    0x96, 0xbb, 0x4b, 0xa0, 0x2e, 0x05, 0x2d, 0xee, 0xeb, 0x9a, 0x1a, 0xae, 0xde, 0xe9, 0x2c, 0xf8,
    0x2f, 0x43, 0x6c, 0x78, 0x4b, 0xde, 0xef, 0x91, 0x8b, 0x94, 0x9d, 0x4f, 0x81, 0x05, 0xcc, 0xf0,
    0x98, 0xce, 0xce, 0x67, 0x54, 0xac, 0xde, 0xcd, 0x26, 0x9e, 0x84, 0xf4, 0x88, 0xb2, 0x1a, 0x3e,
    0x93, 0x2e, 0xff, 0xa8, 0x45, 0x95, 0xd1, 0xd0, 0xb1, 0x6c, 0x3c, 0x1e, 0xef, 0x3d, 0xe3, 0xf2,
    0x73, 0xe2, 0xf6, 0xb7, 0xf9, 0x3f, 0x10, 0x0d, 0x3c, 0xde, 0x28, 0x94, 0x07, 0xef, 0x24, 0x70,
    0xc4, 0x5a, 0x0a, 0x67, 0xbb, 0x0f, 0x4f, 0x5c, 0x2b, 0xd8, 0x02, 0x05, 0xa5, 0x98, 0x03, 0x5d,
    0x8f, 0xc0, 0x4a, 0x84, 0xe9, 0xea, 0xac, 0x13, 0xdf, 0x69, 0xfc, 0x1e, 0xcf, 0xb6, 0x88, 0xba,
    0x99, 0x30, 0xbc, 0x7a, 0xb8, 0x9d, 0x3d, 0x62, 0x3b, 0x33, 0x19, 0xbb, 0x3a, 0x2c, 0x2b, 0xa0,
    0x5a, 0xb0, 0x8f, 0x9e, 0x10, 0x81, 0xb9, 0x12, 0x54, 0x81, 0xf8, 0xe2, 0x91, 0xb2, 0xe7, 0xe6,
    0x9c, 0x11, 0xeb, 0x49, 0x64, 0x3a, 0x25, 0xd6, 0x53, 0x2e, 0xdf, 0xfc, 0x14, 0x32, 0x65, 0xcc,
    0x87, 0xdb, 0xfd, 0xbb, 0x81, 0xa8, 0x50, 0xcc, 0xb4, 0x31, 0x0c, 0x70, 0xf3, 0xb6, 0x15, 0x8a,
    0x50, 0x80, 0xad, 0xb1, 0xb0, 0x10, 0x2b, 0x67, 0x33, 0xf5, 0xf6, 0x36, 0x35, 0x5f, 0xa7, 0xd2,
    0x81, 0xd6, 0x75, 0xa1, 0x18, 0x15, 0xbe, 0x1d, 0x5e, 0x33, 0x8e, 0x98, 0xdd, 0x45, 0x0f, 0x0c,
    0x0f, 0x0d, 0x8b, 0x3f, 0x97, 0x11, 0x21, 0x2e, 0xa0, 0x5e, 0xfe, 0x70, 0x09, 0xa7, 0x14, 0x30,
    0xa3, 0x01, 0x2d, 0x18, 0x2b, 0x8f, 0x19, 0x75, 0x54, 0x1f, 0xd8, 0xee, 0x66, 0x06, 0x7b, 0x9d,
    0x7d, 0xb2, 0xae, 0x14, 0xe6, 0x51, 0x19, 0xc2, 0x45, 0x2e, 0x7e, 0x11, 0xd9, 0x7b, 0x16, 0x8e,
    0xae, 0x17, 0xdb, 0x1b, 0x24, 0x90, 0xcd, 0xed, 0x94, 0xf9, 0xf7, 0xba, 0x9f, 0x4c, 0x12, 0xae,
    0x31, 0x7b, 0xd4, 0x7c, 0x04, 0x42, 0x2c, 0x32, 0x16, 0xc1, 0x70, 0x6d, 0x11, 0x6f, 0x3b, 0x44,
    0x62, 0xba, 0xbd, 0xc5, 0x7a, 0xec, 0x55, 0x1b, 0xcd, 0xdb, 0xb6, 0x55, 0x08, 0x86, 0x13, 0x7f,
    0x4e, 0xa9, 0x63, 0xe1, 0x87, 0xa0, 0x7e, 0x49, 0xfb, 0xf4, 0xa3, 0x46, 0xcf, 0x1d, 0xec, 0xf5,
    0xc6, 0x2f, 0xe1, 0x43, 0x02, 0xd0, 0xe4, 0x5f, 0x1b, 0x20, 0x1a, 0xa7, 0x81, 0xbd, 0x31, 0x19,
    0x6a, 0x74, 0xd7, 0x9a, 0x6d, 0x3d, 0xf8, 0xac, 0x4d, 0xbb, 0x01, 0x63, 0xa4, 0x9d, 0x3c, 0xc9,
    0x6c, 0x8a, 0x4f, 0x61, 0xd6, 0x98, 0xf5, 0x40, 0x22, 0xa9, 0x5e, 0x93, 0x5e, 0x13, 0xd3, 0xe0,
    0xdb, 0x54, 0xab, 0x0d, 0xe3, 0x88, 0x85, 0x80, 0x7a, 0x5e, 0x38, 0x64, 0x97, 0xc4, 0xe9, 0xb0,
    0x5d, 0xf7, 0x40, 0x5f, 0x6e, 0x3f, 0xbe, 0x14, 0x9a, 0x7c, 0xa2, 0x7c, 0x74, 0xa5, 0x32, 0x22,
    0x61, 0x31, 0xa5, 0x0d, 0xa5, 0xcc, 0x93, 0xe4, 0xfd, 0xed, 0xbc, 0xe7, 0xf2, 0xe5, 0xdb, 0xc6,
    0x0c, 0xe1, 0xc8, 0x4e, 0xee, 0xe6, 0x76, 0x1c, 0x10, 0x1b, 0xd8, 0x53, 0xd4, 0xe8, 0x07, 0xed,
    0xea, 0x91, 0xd4, 0x1b, 0x91, 0x5c, 0x28, 0x05, 0xca, 0xe2, 0x9c, 0xd1, 0x99, 0x43, 0xed, 0xd8,
    0x6a, 0x2b, 0xd2, 0x64, 0x9b, 0xe1, 0x0c, 0x88, 0x6c, 0x0d, 0xb2, 0x6b, 0x73, 0x85, 0x9d, 0xbf,
    0x79, 0x78, 0xaa, 0x7b, 0x5e, 0xf8, 0xa4, 0x26, 0xdf, 0xb3, 0x9b, 0x24, 0x5a, 0xc8, 0x19, 0x22,
    0xa5, 0xc6, 0xca, 0x00, 0x59, 0x3b, 0xad, 0x45, 0xdf, 0x71, 0x1d, 0x60, 0x60, 0x24, 0x0d, 0xa4,
    0x3d, 0x42, 0x23, 0xb8, 0xfe, 0xac, 0x86, 0x94, 0x79, 0x87, 0x05, 0xae, 0xb8, 0x4d, 0x7e, 0x11,
    0x5b, 0x22, 0x44, 0x15, 0x3d, 0x7f, 0x82, 0x98, 0x65, 0x0a, 0x3c, 0xd3, 0xef, 0x80, 0x0d, 0x75,
    0x03, 0x92, 0xf6, 0x3a, 0x8b, 0xa4, 0xb0, 0x61, 0x2d, 0x2c, 0xcc, 0x1f, 0x01, 0x8e, 0x7a, 0x46,
    0x36, 0x2a, 0x83, 0x21, 0x88, 0x98, 0x13, 0x0a, 0xd5, 0xa1, 0x54, 0x4b, 0x63, 0xe0, 0xe3, 0x1c,
    0x07, 0x5e, 0x32, 0x8c, 0xa4, 0x6b, 0x62, 0xc3, 0x28, 0x95, 0xb8, 0x0a, 0xb9, 0x4f, 0xaf, 0x7f,
    0x49, 0xeb, 0xff, 0xd7, 0xa1, 0x41, 0x43, 0x9a, 0x92, 0x9a, 0x5f, 0xee, 0xbd, 0xb9, 0xbe, 0xb3,
    0x4b, 0x9d, 0x0b, 0xcb, 0x9b, 0x2c, 0x26, 0x8f, 0x0f, 0xb1, 0xfa, 0xc0, 0xe3, 0x3a, 0x7f, 0x2b,
    0x51, 0x79, 0x75, 0x25, 0x5b, 0x23, 0x22, 0xb1, 0x01, 0x27, 0x4e, 0x43, 0xdd, 0x66, 0x7a, 0x33,
    0x4d, 0x32, 0x96, 0x83, 0x59, 0x52, 0xd7, 0x3c, 0xb0, 0xe3, 0x03, 0xd6, 0xb0, 0xc7, 0x99, 0x68,
    0xc6, 0xa4, 0x2d, 0x35, 0x3d, 0xa4, 0x6a, 0x17, 0xd9, 0xf4, 0x0c, 0x26, 0x11, 0xe4, 0xbc, 0x03,
    0x87, 0x25, 0x62, 0xad, 0xa9, 0x7e, 0x96, 0x4d, 0x39, 0x9b, 0x8f, 0x09, 0xdc, 0xd1, 0x28, 0x5e,
    0xf4, 0xe3, 0x94, 0xfd, 0x94, 0x46, 0x30, 0xe2, 0x24, 0x46, 0x30, 0x7f, 0xf4, 0x4c, 0xaa, 0x51,
    0x7e, 0x04, 0x5c, 0xa4, 0x8c, 0xba, 0x4a, 0xb8, 0x61, 0x5e, 0x75, 0x1c, 0xa8, 0x0c, 0xbc, 0x7f,
    0x36, 0x16, 0xa1, 0x72, 0x98, 0x6a, 0x44, 0x39, 0x42, 0x67, 0xb5, 0x4a, 0xac, 0x14, 0x35, 0x8f,
    0xcd, 0x87, 0x3f, 0x9e, 0x2e, 0xa1, 0x53, 0xf1, 0x45, 0x68, 0x26, 0xcb, 0x35, 0x96, 0x57, 0xd5,
    0x3a, 0x24, 0x74, 0xe2, 0xff, 0xe0, 0x70, 0xb1, 0xbd, 0xec, 0x0c, 0xd2, 0x97, 0x9a, 0xe5, 0x9f,
    0xa9, 0xfe, 0x6a, 0x63, 0x17, 0x35, 0xad, 0x64, 0x2f, 0xd9, 0x2e, 0xdb, 0x47, 0xdc, 0x62, 0xdc,
    0xcc, 0xee, 0x7e, 0x23, 0xa6, 0x67, 0x61, 0x7c, 0xd1, 0x03, 0xbd, 0x78, 0xe9, 0x34, 0x05, 0xed,
    0x05, 0x87, 0xef, 0x59, 0xf4, 0x16, 0xd6, 0x8d, 0x85, 0x46, 0x65, 0x2a, 0x08, 0xac, 0x4a, 0x5d,
    0xe6, 0x27, 0x5f, 0x43, 0xdd, 0x51, 0x4e, 0x95, 0x9b, 0xf5, 0x0c, 0x81, 0x24, 0x73, 0x39, 0x77,
    0xe9, 0xc8, 0x35, 0x4a, 0xe2, 0xb8, 0x35, 0x92, 0xde, 0x5c, 0x31, 0x12, 0x36, 0x5c, 0xc7, 0x69,
    0xcd, 0x79, 0xa9, 0xf9, 0xcf, 0x13, 0xa9, 0x12, 0x29, 0x25, 0x5c, 0x6a, 0x34, 0xa4, 0xbf, 0xc5,
    0xb6, 0x2a, 0xc1, 0xba, 0x6a, 0xd3, 0x98, 0x8c, 0x9b, 0x6d, 0x9f, 0xb9, 0x25, 0xa6, 0xd1, 0x97,
    0x80, 0x38, 0x11, 0xdc, 0x73, 0x5c, 0xe7, 0x3a, 0x1f, 0xd2, 0x16, 0xcd, 0x63, 0xfb, 0x41, 0xb0,
    0xba, 0xb0, 0x38, 0x67, 0x48, 0xd2, 0x8a, 0x94, 0x2f, 0x11, 0x81, 0xbf, 0x66, 0x38, 0x68, 0xff,
    0xfe, 0xd1, 0x7c, 0xcd, 0xa3, 0xac, 0xe4, 0xf7, 0x58, 0x19, 0xcd, 0x2a, 0xe3, 0xfa, 0x4d, 0xb0,
    0xbe, 0xac, 0x05, 0x1c, 0xd9, 0x8d, 0xf7, 0x5c, 0xc0, 0xfc, 0xa6, 0xb5, 0x99, 0xb8, 0x8e, 0x2b,
    0x72, 0xf8, 0x19, 0xfc, 0x17, 0x11, 0xf6, 0x2b, 0x08, 0xe4, 0x6e, 0xb0, 0x65, 0xab, 0x78, 0x8a,
    0xfc, 0x7c, 0x09, 0xca, 0x73, 0xcd, 0x35, 0x5d, 0x6c, 0x7a, 0x36, 0xc0, 0x24, 0xba, 0x3f, 0x08,
    0xea, 0x17, 0x09, 0xe1, 0x9d, 0x5d, 0x18, 0x59, 0x8a, 0xd8, 0x6a, 0x6d, 0x85, 0x6a, 0x9e, 0xa9,
    0xe5, 0x4b, 0x45, 0xb2, 0x35, 0x6e, 0x62, 0x24, 0x08, 0x00, 0x1c, 0x06, 0x73, 0x27, 0x5d, 0x11,
    0x4a, 0xc8, 0x51, 0xbd, 0x59, 0xd6, 0x94, 0xce, 0x16, 0x15, 0x17, 0x58, 0x7f, 0x39, 0x9d, 0x4e,
    0x69, 0x1a, 0x64, 0xbb, 0xd4, 0x51, 0xb9, 0xe4, 0x7d, 0x51, 0x3a, 0xff, 0xe5, 0x1f, 0x29, 0xea,
    0x7e, 0xa5, 0x62, 0x63, 0xff, 0x10, 0xf7, 0x54, 0x35, 0xd1, 0xf3, 0x73, 0x1e, 0xab, 0xca, 0x52,
    0x14, 0xc6, 0x7e, 0x51, 0xc2, 0x48, 0x13, 0xcb, 0x30, 0xb2, 0x1a, 0x84, 0x72, 0xe5, 0x44, 0x83,
    0xc9, 0x90, 0xa5, 0x8c, 0xf9, 0xeb, 0x3c, 0x5c, 0xc6, 0xcc, 0x8a, 0x95, 0x8a, 0xfa, 0xeb, 0x37,
    0x9c, 0xde, 0xa2, 0xb1, 0x72, 0x4d, 0xd9, 0x3d, 0xab, 0xfd, 0x0e, 0xbd, 0x32, 0x9d, 0x23, 0xe9,
    0x6f, 0x85, 0x4e, 0xfe, 0xcd, 0x91, 0xfb, 0x82, 0x94, 0xee, 0x8b, 0xdf, 0x6a, 0xd9, 0x01, 0xa1,
    0xc6, 0x22, 0x18, 0x01, 0x8d, 0x10, 0xd5, 0x87, 0x42, 0xd0, 0xbd, 0x23, 0x75, 0x44, 0x53, 0x46,
    0xa5, 0xae, 0x00, 0x4c, 0x0e, 0x88, 0x4a, 0xa8, 0x3d, 0x4a, 0x30, 0xe0, 0x1a, 0xa4, 0xe5, 0x40,
    0xb8, 0xe0, 0x12, 0x9c, 0x44, 0x03, 0xfb, 0x2e, 0x4e, 0xf5, 0x29, 0xdb, 0x09, 0x84, 0x55, 0xc7,
    0x6c, 0xc6, 0x1f, 0xf9, 0xee, 0x0b, 0xa4, 0x91, 0x7d, 0x79, 0x27, 0x59, 0x75, 0x97, 0xec, 0x6a,
    0xa8, 0xf8, 0x55, 0xa8, 0x45, 0xd4, 0xd7, 0xa6, 0xc1, 0xc4, 0x27, 0x35, 0xe8, 0x4f, 0x39, 0x89,
    0x7d, 0x41, 0xf3, 0xf6, 0xd0, 0xb6, 0xf9, 0x91, 0xeb, 0x94, 0xf1, 0xbb, 0x17, 0x46, 0x9c, 0xd5,
    0x5a, 0x53, 0x04, 0x2d, 0x12, 0x7c, 0x17, 0x6a, 0x36, 0xb5, 0xea, 0xf3, 0x5b, 0x96, 0x1b, 0xee,
    0xce, 0xc4, 0xc0, 0x11, 0x5a, 0xbc, 0x0c, 0x29, 0xd0, 0x42, 0x1d, 0x16, 0x63, 0xea, 0x1e, 0x04,
    0x2f, 0xe3, 0x17, 0xed, 0x33, 0xac, 0x56, 0x80, 0x34, 0x41, 0x41, 0x1e, 0x77, 0x80, 0x06, 0x9f,
    0xbc, 0x2e, 0x78, 0xa1, 0x04, 0x00, 0x06, 0x6f, 0x36, 0x2f, 0xb7, 0xa5, 0x95, 0x37, 0x82, 0x9d,
    0xef, 0x41, 0x08, 0x85, 0x3d, 0x53, 0xa7, 0xfb, 0xfe, 0xba, 0x8c, 0xb9, 0xae, 0xc7, 0x89, 0x11,
    0x69, 0x4f, 0x62, 0xe6, 0xb6, 0x08, 0x6b, 0x35, 0x1c, 0x96, 0xb3, 0x7b, 0x40, 0x2d, 0xee, 0x07,
    0x40, 0x52, 0x4f, 0x68, 0x60, 0xf4, 0xb9, 0xc3, 0x54, 0x9f, 0x22, 0x50, 0x88, 0x48, 0x6a, 0x28,
    0x93, 0x46, 0x00, 0xe2, 0x4a, 0x85, 0x41, 0x78, 0x0e, 0x87, 0xc5, 0xeb, 0xfc, 0xd3, 0x5f, 0x4d,
    0x24, 0xe4, 0x9d, 0xeb, 0x1d, 0x00, 0x73, 0x85, 0x25, 0x47, 0x9e, 0x8c, 0x5b, 0x88, 0xf4, 0x3b,
    0x33, 0xf0, 0x3d, 0x3a, 0xa1, 0x28, 0xd3, 0x06, 0xb4, 0x7a, 0x4e, 0x5d, 0x31, 0x1b, 0xca, 0xf4,
    0x3f, 0x70, 0x30, 0x49, 0x44, 0x29, 0x24, 0x14, 0x5e, 0x35, 0xc2, 0x6c, 0x92, 0x7e, 0xf8, 0x97,
    0x0c, 0x51, 0x9d, 0x67, 0xc0, 0x10, 0xa9, 0x35, 0x48, 0x59, 0x6a, 0x33, 0xef, 0x40, 0x4e, 0x53,
    0x10, 0x14, 0x2a, 0x12, 0x38, 0xe6, 0xc4, 0x63, 0x9c, 0x84, 0x85, 0x06, 0xaf, 0x3d, 0x3a, 0x84,
    0x06, 0x60, 0x88, 0x32, 0xda, 0x2c, 0xe5, 0xc6, 0x59, 0xf1, 0xe0, 0x10, 0xe2, 0x3c, 0xe6, 0xbf,
    0x32, 0x7d, 0x32, 0x39, 0x6d, 0xe4, 0xd9, 0xca, 0xe7, 0xf5, 0xf4, 0xa6, 0x5f, 0xb2, 0x33, 0x05,
    0xb5, 0xad, 0x5f, 0xcb, 0x0b, 0x14, 0xaf, 0xeb, 0xc0, 0xec, 0x87, 0x85, 0x9b, 0x13, 0xb5, 0x8a,
    0x98, 0xa9, 0x92, 0x13, 0x1b, 0x74, 0xec, 0xfd, 0xe1, 0xc1, 0x22, 0x06, 0x5d, 0x4f, 0x06, 0xc7,
    0xdd, 0xc6, 0xf0, 0xc4, 0x01, 0x04, 0xad, 0x7f, 0x71, 0xbc, 0x74, 0x4d, 0xfd, 0x18, 0xa3, 0x56,
    0x2c, 0x45, 0x28, 0x2b, 0x2f, 0xbc, 0x9b, 0xb8, 0x4b, 0xe6, 0x51, 0x75, 0x28, 0x0c, 0x27, 0x0e,
    0xf7, 0x92, 0x8c, 0xc9, 0xde, 0x33, 0x1a, 0x65, 0x28, 0xc7, 0x01, 0x32, 0xa2, 0x36, 0x88, 0xb6,
    0x64, 0x10, 0x03, 0xd6, 0xb7, 0x9f, 0x9d, 0x73, 0xe1, 0xa9, 0xc7, 0xdf, 0xe1, 0x0b, 0x39, 0x31,
    0x77, 0xbc, 0x91, 0xf1, 0x45, 0x9a, 0xc5, 0x97, 0x28, 0xc0, 0x61, 0xc5, 0x23, 0x54, 0xad, 0xe3,
    0x23, 0x18, 0x69, 0xf7, 0x27, 0xd0, 0x5b, 0xf2, 0x44, 0x62, 0xdc, 0x97, 0xce, 0x4e, 0x40, 0x76,
    0x00, 0xde, 0xc2, 0xf9, 0x3a, 0x42, 0xfd, 0xd4, 0xd7, 0xe1, 0x85, 0xd8, 0xc9, 0x38, 0x91, 0xc1,
    0x79, 0x87, 0x58, 0xf1, 0x26, 0x1a, 0x29, 0x02, 0xe3, 0x54, 0xde, 0x58, 0x64, 0x9d, 0xe6, 0x8e,
    0x33, 0x70, 0x53, 0x43, 0x47, 0x90, 0xee, 0x6e, 0x0f, 0x8c, 0xb3, 0x9e, 0x47, 0x45, 0xfc, 0xa8,
    0xe3, 0x52, 0x62, 0x74, 0x6d, 0xa2, 0xaf, 0x28, 0x9d, 0xdf, 0x1e, 0x69, 0x1f, 0x56, 0xbc, 0x49,
    0xc1, 0xe5, 0xd6, 0xc4, 0xb5, 0x5c, 0x4d, 0x39, 0x49, 0x4b, 0xb4, 0xec, 0x56, 0x54, 0x9a, 0x15,
    0x94, 0x0a, 0xcb, 0xa9, 0x10, 0x46, 0x03, 0x5c, 0x23, 0x2f, 0x29, 0xed, 0x72, 0xa1, 0x57, 0xfa,
    0x58, 0xef, 0x21, 0x7e, 0xf2, 0x8b, 0xa7, 0x04, 0x51, 0xb4, 0x03, 0x5d, 0xd8, 0x48, 0xc0, 0xe5,
    0x83, 0xb6, 0x7a, 0x6b, 0xcd, 0xfb, 0xda, 0x47, 0xe8, 0xa1, 0xae, 0x57, 0x74, 0x49, 0xc0, 0xf9,
    0x4a, 0x6b, 0x3c, 0xb5, 0xd8, 0x27, 0x3d, 0x1d, 0x96, 0x39, 0x09, 0x65, 0x95, 0xdb, 0x01, 0xa3,
    0x8b, 0x78, 0x8b, 0x07, 0x6d, 0x1c, 0x8b, 0x4b, 0x1d, 0x9d, 0x4a, 0x4f, 0xcb, 0xb8, 0xf6, 0x22,
    0x73, 0x8a, 0x7b, 0xc8, 0xf2, 0x0a, 0xef, 0x03, 0x1e, 0xb7, 0x4d, 0x8f, 0xc0, 0xdf, 0x87, 0x88,
    0x05, 0xe1, 0x0a, 0x30, 0xea, 0xde, 0xf3, 0xc2, 0xb6, 0x00, 0x3c, 0xd6, 0xff, 0x3b, 0xb5, 0x01,
    0xfb, 0xd8, 0xb2, 0x65, 0x26, 0x5d, 0xa0, 0x5a, 0x7c, 0xef, 0x1d, 0x85, 0xbe, 0x51, 0xc2, 0x57,
    0x0b, 0x27, 0x37, 0x71, 0x99, 0xf5, 0x87, 0x83, 0x68, 0x0b, 0x88, 0xed, 0x66, 0x9e, 0x37, 0x59,
    0x84, 0x23, 0x72, 0xc3, 0x80, 0xac, 0xfe, 0x45, 0x5f, 0xdf, 0x31, 0xc4, 0x84, 0x07, 0x5a, 0x17,
    0x28, 0xcd, 0x64, 0xb4, 0xe2, 0xa3, 0x0e, 0x2c, 0x15, 0x60, 0x77, 0xdc, 0x08, 0x45, 0x36, 0x37,
    0x68, 0x50, 0xba, 0x03, 0x85, 0xb7, 0xed, 0xd0, 0x7b, 0xb2, 0xa1, 0x62, 0xbc, 0x70, 0x00, 0x9e,
];

/// Result of checking whether an update is available.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateCheck {
    pub current_version: String,
    pub latest_version: String,
    pub update_available: bool,
    pub download_url: Option<String>,
}

impl UpdateCheck {
    /// Force the check to report an update even if already on the latest version.
    ///
    /// Populates `download_url` using the current `latest_version`.
    pub fn force(&mut self) -> Result<()> {
        self.update_available = true;
        self.download_url = Some(build_download_url(&self.latest_version)?);
        Ok(())
    }
}

/// Result of a completed update.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateResult {
    pub previous_version: String,
    pub new_version: String,
}

/// Check whether a newer version is available on GitHub Releases.
///
/// Compares `current_version` against the latest release tag using semantic versioning.
pub async fn check_for_update(current_version: &str) -> Result<UpdateCheck> {
    let latest = fetch_latest_cli_version().await?;
    let current = parse_version(current_version)?;
    let latest_parsed = parse_version(&latest)?;
    let update_available = latest_parsed > current;

    let download_url = if update_available {
        Some(build_download_url(&latest)?)
    } else {
        None
    };

    Ok(UpdateCheck {
        current_version: current_version.to_string(),
        latest_version: latest,
        update_available,
        download_url,
    })
}

/// Download and install the update, replacing the current binary.
///
/// Downloads the release archive and its detached `.sig` file, verifies the
/// ML-DSA-65 signature against the embedded release key, extracts the binary,
/// and replaces the running executable.
pub async fn perform_update(
    check: &UpdateCheck,
    progress: &dyn ProgressReporter,
) -> Result<UpdateResult> {
    let download_url = check.download_url.as_deref().ok_or_else(|| {
        Error::UpdateFailed("no download URL — are you already on the latest version?".to_string())
    })?;

    let tmp_dir = tempfile::tempdir()
        .map_err(|e| Error::UpdateFailed(format!("failed to create temp directory: {e}")))?;

    let (archive_path, archive_bytes) =
        download_archive(download_url, tmp_dir.path(), progress).await?;

    let sig_url = format!("{download_url}.sig");
    progress.report_started("Downloading signature...");
    let sig_bytes = download_bytes(&sig_url).await?;

    progress.report_started("Verifying ML-DSA signature...");
    verify_signature(&archive_bytes, &sig_bytes)?;
    progress.report_complete("Signature verified");

    progress.report_started("Extracting archive...");
    let extracted = if download_url.ends_with(".zip") {
        extract_zip(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
    } else {
        extract_tar_gz(&archive_bytes, tmp_dir.path(), CLI_BINARY_NAME)?
    };
    let binary_path = extracted.binary_path;

    // Verify the extracted binary reports the expected version.
    let actual_version = extract_version(&binary_path).await;
    if let Ok(ref v) = actual_version {
        if v != &check.latest_version {
            return Err(Error::UpdateFailed(format!(
                "version mismatch: expected {}, binary reports {v}",
                check.latest_version
            )));
        }
    }

    replace_binary(&binary_path)?;

    // Clean up is handled by tmp_dir Drop, but remove the archive explicitly
    // since it can be large.
    let _ = std::fs::remove_file(&archive_path);

    Ok(UpdateResult {
        previous_version: check.current_version.clone(),
        new_version: check.latest_version.clone(),
    })
}

/// Fetch the latest stable CLI release version from GitHub.
///
/// Lists all releases and finds the newest non-draft, non-prerelease one whose
/// tag starts with `ant-cli-v`.
async fn fetch_latest_cli_version() -> Result<String> {
    // per_page=100 is the GitHub API maximum; covers repos with many release tags.
    let url = format!("https://api.github.com/repos/{GITHUB_REPO}/releases?per_page=100");
    let client = reqwest::Client::new();
    let resp = client
        .get(&url)
        .header("User-Agent", "ant-cli")
        .header("Accept", "application/vnd.github+json")
        .send()
        .await
        .map_err(|e| Error::UpdateFailed(format!("failed to fetch releases: {e}")))?;

    if !resp.status().is_success() {
        return Err(Error::UpdateFailed(format!(
            "GitHub API returned status {} when fetching releases",
            resp.status()
        )));
    }

    let releases: Vec<serde_json::Value> = resp
        .json()
        .await
        .map_err(|e| Error::UpdateFailed(format!("failed to parse releases JSON: {e}")))?;

    // Find the newest stable ant-cli release by semver (skip drafts and pre-releases).
    let mut best: Option<semver::Version> = None;
    for release in &releases {
        if release["draft"].as_bool().unwrap_or(false)
            || release["prerelease"].as_bool().unwrap_or(false)
        {
            continue;
        }
        let tag = release["tag_name"].as_str().unwrap_or_default();
        if let Some(version_str) = tag.strip_prefix(TAG_PREFIX) {
            if let Ok(v) = semver::Version::parse(version_str) {
                if best.as_ref().is_none_or(|b| v > *b) {
                    best = Some(v);
                }
            }
        }
    }

    best.map(|v| v.to_string())
        .ok_or_else(|| Error::UpdateFailed("no ant-cli release found on GitHub".to_string()))
}

/// Download a release archive to a temp directory, streaming to disk.
///
/// Returns the path to the downloaded file and the raw bytes (needed for
/// signature verification before extraction).
async fn download_archive(
    url: &str,
    tmp_dir: &Path,
    progress: &dyn ProgressReporter,
) -> Result<(PathBuf, Vec<u8>)> {
    progress.report_started(&format!("Downloading {CLI_BINARY_NAME} from {url}"));

    let client = reqwest::Client::new();
    let resp = client
        .get(url)
        .header("User-Agent", "ant-cli")
        .send()
        .await
        .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;

    if !resp.status().is_success() {
        return Err(Error::UpdateFailed(format!(
            "download returned status {}",
            resp.status()
        )));
    }

    let total_size = resp.content_length().unwrap_or(0);
    let mut downloaded: u64 = 0;

    let tmp_archive = tmp_dir.join(".download.tmp");
    let mut tmp_file = std::fs::File::create(&tmp_archive)
        .map_err(|e| Error::UpdateFailed(format!("failed to create temp file: {e}")))?;

    let mut stream = resp.bytes_stream();
    while let Some(chunk) = stream.next().await {
        let chunk =
            chunk.map_err(|e| Error::UpdateFailed(format!("download stream error: {e}")))?;
        downloaded += chunk.len() as u64;
        std::io::Write::write_all(&mut tmp_file, &chunk)
            .map_err(|e| Error::UpdateFailed(format!("failed to write temp file: {e}")))?;
        progress.report_progress(downloaded, total_size);
    }
    drop(tmp_file);

    progress.report_complete("Download complete");

    let bytes = std::fs::read(&tmp_archive)
        .map_err(|e| Error::UpdateFailed(format!("failed to read temp file: {e}")))?;

    Ok((tmp_archive, bytes))
}

/// Download a small file (e.g., a `.sig` signature) into memory.
async fn download_bytes(url: &str) -> Result<Vec<u8>> {
    let client = reqwest::Client::new();
    let resp = client
        .get(url)
        .header("User-Agent", "ant-cli")
        .send()
        .await
        .map_err(|e| Error::UpdateFailed(format!("download request failed: {e}")))?;

    if !resp.status().is_success() {
        return Err(Error::UpdateFailed(format!(
            "download returned status {} for {url}",
            resp.status()
        )));
    }

    resp.bytes()
        .await
        .map(|b| b.to_vec())
        .map_err(|e| Error::UpdateFailed(format!("failed to read response body: {e}")))
}

/// Verify the ML-DSA-65 signature on an archive using the embedded release key.
fn verify_signature(archive_bytes: &[u8], signature_bytes: &[u8]) -> Result<()> {
    if signature_bytes.len() != SIGNATURE_SIZE {
        return Err(Error::UpdateFailed(format!(
            "invalid signature size: expected {SIGNATURE_SIZE}, got {}",
            signature_bytes.len()
        )));
    }

    let public_key = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, RELEASE_SIGNING_KEY)
        .map_err(|e| Error::UpdateFailed(format!("invalid embedded release key: {e}")))?;

    let sig = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, signature_bytes)
        .map_err(|e| Error::UpdateFailed(format!("invalid signature format: {e}")))?;

    let dsa = ml_dsa_65();
    let valid = dsa
        .verify_with_context(&public_key, archive_bytes, &sig, SIGNING_CONTEXT)
        .map_err(|e| Error::UpdateFailed(format!("signature verification error: {e}")))?;

    if valid {
        Ok(())
    } else {
        Err(Error::UpdateFailed(
            "signature verification failed: archive may be corrupted or tampered".to_string(),
        ))
    }
}

/// Extract the version string from a binary by running `<binary> --version`.
async fn extract_version(binary_path: &Path) -> Result<String> {
    let mut cmd = tokio::process::Command::new(binary_path);
    cmd.arg("--version");
    // CREATE_NO_WINDOW: prevents Windows from allocating a console window for
    // the console-subsystem child binary. Without this, every version probe
    // flashes a window — visible as "ghost flashes" in GUI consumers.
    #[cfg(windows)]
    {
        const CREATE_NO_WINDOW: u32 = 0x08000000;
        cmd.creation_flags(CREATE_NO_WINDOW);
    }
    let output = cmd.output().await.map_err(|e| {
        Error::UpdateFailed(format!(
            "failed to run {} --version: {e}",
            binary_path.display()
        ))
    })?;

    if !output.status.success() {
        return Err(Error::UpdateFailed(format!(
            "{} --version exited with status {}",
            binary_path.display(),
            output.status
        )));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    Ok(parse_version_from_stdout(&stdout).to_string())
}

/// Extract the version token from `--version` stdout.
///
/// Only the first line is inspected: historically `ant --version` was a single
/// line like `ant 0.1.4`, but a richer multi-line long-version string ending
/// with `License: MIT or Apache-2.0` shipped briefly and caused this parser —
/// in 0.1.2 through 0.1.4 — to pick up `Apache-2.0` as the version. Scoping
/// to the first line keeps us compatible with both forms.
fn parse_version_from_stdout(stdout: &str) -> &str {
    stdout
        .lines()
        .next()
        .unwrap_or_default()
        .split_whitespace()
        .last()
        .unwrap_or("unknown")
}

/// Replace the current executable with the new binary.
///
/// Uses the `self_replace` crate which handles platform-specific nuances,
/// particularly on Windows where the running executable is locked.
fn replace_binary(new_binary: &Path) -> Result<()> {
    self_replace::self_replace(new_binary)
        .map_err(|e| Error::UpdateFailed(format!("failed to replace binary: {e}")))?;
    Ok(())
}

/// Build the download URL for a given version.
///
/// Tag format: `ant-cli-v{version}`
/// Asset format: `ant-{version}-{target_triple}.{ext}`
pub fn build_download_url(version: &str) -> Result<String> {
    let asset_name = cli_platform_asset_name(version)?;
    Ok(format!(
        "https://github.com/{GITHUB_REPO}/releases/download/{TAG_PREFIX}{version}/{asset_name}"
    ))
}

/// Returns the platform-specific archive asset name for the CLI binary.
///
/// Matches the naming convention from CI: `ant-{version}-{target_triple}.{ext}`
fn cli_platform_asset_name(version: &str) -> Result<String> {
    let target_triple = if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
        "x86_64-unknown-linux-musl"
    } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
        "aarch64-unknown-linux-musl"
    } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
        "x86_64-apple-darwin"
    } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
        "aarch64-apple-darwin"
    } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
        "x86_64-pc-windows-msvc"
    } else {
        return Err(Error::UpdateFailed(format!(
            "unsupported platform: {}-{}",
            std::env::consts::OS,
            std::env::consts::ARCH
        )));
    };

    let ext = if cfg!(target_os = "windows") {
        "zip"
    } else {
        "tar.gz"
    };

    Ok(format!("ant-{version}-{target_triple}.{ext}"))
}

fn parse_version(version: &str) -> Result<semver::Version> {
    let cleaned = version.strip_prefix('v').unwrap_or(version);
    semver::Version::parse(cleaned)
        .map_err(|e| Error::UpdateFailed(format!("invalid version '{version}': {e}")))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_version_from_stdout_single_line() {
        assert_eq!(parse_version_from_stdout("ant 0.1.4"), "0.1.4");
        assert_eq!(parse_version_from_stdout("ant 0.1.4\n"), "0.1.4");
    }

    #[test]
    fn parse_version_from_stdout_multi_line_license_trailer() {
        // This is the exact shape that broke 0.1.2–0.1.4 self-update.
        let stdout = "ant 0.1.4\n\
            Autonomi network client\n\
            \n\
            Repository: https://github.com/WithAutonomi/ant-client\n\
            License:    MIT or Apache-2.0\n";
        assert_eq!(parse_version_from_stdout(stdout), "0.1.4");
    }

    #[test]
    fn parse_version_from_stdout_empty() {
        assert_eq!(parse_version_from_stdout(""), "unknown");
    }

    #[test]
    fn parse_version_from_stdout_blank_first_line() {
        assert_eq!(parse_version_from_stdout("\nant 0.1.4\n"), "unknown");
    }

    #[test]
    fn parse_version_from_stdout_leading_whitespace() {
        assert_eq!(parse_version_from_stdout("   ant 0.1.4\n"), "0.1.4");
    }

    #[test]
    fn parse_version_valid() {
        assert!(parse_version("1.2.3").is_ok());
        assert!(parse_version("v1.2.3").is_ok());
        assert!(parse_version("0.1.0").is_ok());
    }

    #[test]
    fn parse_version_invalid() {
        assert!(parse_version("not-a-version").is_err());
        assert!(parse_version("").is_err());
    }

    #[test]
    fn version_comparison() {
        let v1 = parse_version("0.1.0").unwrap();
        let v2 = parse_version("0.2.0").unwrap();
        assert!(v2 > v1);

        let v3 = parse_version("1.0.0").unwrap();
        assert!(v3 > v2);

        let same = parse_version("0.1.0").unwrap();
        assert_eq!(v1, same);
    }

    #[test]
    fn check_result_no_update() {
        let check = UpdateCheck {
            current_version: "1.0.0".to_string(),
            latest_version: "1.0.0".to_string(),
            update_available: false,
            download_url: None,
        };
        assert!(!check.update_available);
        assert!(check.download_url.is_none());
    }

    #[test]
    fn check_result_with_update() {
        let check = UpdateCheck {
            current_version: "0.1.0".to_string(),
            latest_version: "0.2.0".to_string(),
            update_available: true,
            download_url: Some("https://example.com/ant.tar.gz".to_string()),
        };
        assert!(check.update_available);
        assert!(check.download_url.is_some());
    }

    #[test]
    fn force_populates_download_url() {
        let mut check = UpdateCheck {
            current_version: "1.0.0".to_string(),
            latest_version: "1.0.0".to_string(),
            update_available: false,
            download_url: None,
        };
        check.force().unwrap();
        assert!(check.update_available);
        assert!(check.download_url.is_some());
    }

    #[test]
    fn platform_asset_name_format() {
        let name = cli_platform_asset_name("1.2.3").unwrap();
        assert!(name.starts_with("ant-1.2.3-"));
        assert!(
            name.ends_with(".tar.gz") || name.ends_with(".zip"),
            "unexpected extension: {name}"
        );
    }

    #[test]
    fn build_download_url_format() {
        let url = build_download_url("1.2.3").unwrap();
        assert!(url.starts_with(
            "https://github.com/WithAutonomi/ant-client/releases/download/ant-cli-v1.2.3/ant-1.2.3-"
        ));
        assert!(url.ends_with(".tar.gz") || url.ends_with(".zip"));
    }

    #[test]
    fn update_check_serializes() {
        let check = UpdateCheck {
            current_version: "0.1.0".to_string(),
            latest_version: "0.2.0".to_string(),
            update_available: true,
            download_url: Some("https://example.com/ant.tar.gz".to_string()),
        };
        let json = serde_json::to_string(&check).unwrap();
        let deserialized: UpdateCheck = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.current_version, "0.1.0");
        assert!(deserialized.update_available);
    }

    #[test]
    fn verify_signature_rejects_wrong_size() {
        let result = verify_signature(b"some archive data", &[0u8; 100]);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("invalid signature size"), "got: {err}");
    }

    #[test]
    fn verify_signature_rejects_invalid_signature() {
        let invalid_sig = vec![0u8; SIGNATURE_SIZE];
        let result = verify_signature(b"some archive data", &invalid_sig);
        assert!(result.is_err());
    }

    #[test]
    fn verify_signature_valid_roundtrip() {
        let dsa = ml_dsa_65();
        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
        let archive = b"fake archive content for testing";

        let sig = dsa
            .sign_with_context(&secret_key, archive, SIGNING_CONTEXT)
            .unwrap();

        // Use the _with_key variant for testing (can't use embedded key)
        let parsed_sig =
            MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig.to_bytes()).unwrap();
        let valid = dsa
            .verify_with_context(&public_key, archive, &parsed_sig, SIGNING_CONTEXT)
            .unwrap();
        assert!(valid);
    }
}
