//! `iddrop` CLI — skeleton commands for the iddrop protocol.
//!
//! v0.1 wires the protocol-engine to a CLI surface. Real transport
//! integration (NFC / brain-api) follows in v0.2+.

use clap::{Parser, Subcommand};
use tibet_iddrop::{
    claim::SemanticType,
    nonce::ChallengeNonce,
    offer::Offer,
    request::Request,
    validator::{validate, ReceiverContext},
    IdClaim, IddropError, PROTOCOL_VERSION, SPEC_REFERENCE,
};

#[derive(Parser)]
#[command(
    name = "iddrop",
    version,
    about = "Identity Drop and Acceptance Protocol — offer-first + request-first",
    long_about = format!(
        "iddrop: {}\n\
         Implements {} (= IETF Internet-Draft, work in progress).\n\
         \n\
         Two modes:\n  \
           offer-first  — proximity / NFC / QR (humaan)\n  \
           request-first — wire / cap-bus / HTTP (autonoom)",
        PROTOCOL_VERSION, SPEC_REFERENCE
    )
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Open a proximity offer (= offer-first mode).
    Offer {
        /// AINS handle being offered, e.g. `jasper.aint`.
        #[arg(long)]
        identity: String,
        /// Sender's Ed25519 pubkey (hex, 64 chars).
        #[arg(long)]
        sender_pubkey: String,
        /// Display name shown in the receiver's accept dialog.
        #[arg(long)]
        display_name: String,
        /// Offer TTL in seconds (= default 60s).
        #[arg(long, default_value_t = 60)]
        ttl: u64,
    },

    /// Render an offer as a compact base64 payload + scannable URL.
    ///
    /// Output formats:
    ///   --format url   → `iddrop://<base64>` (scannable as QR)  [default]
    ///   --format json  → pretty JSON of the Offer object
    ///   --format raw   → bare base64 string (= for piping into qrencode)
    OfferQr {
        /// AINS handle being offered.
        #[arg(long)]
        identity: String,
        /// Sender's Ed25519 pubkey (hex, 64 chars).
        #[arg(long)]
        sender_pubkey: String,
        /// Display name shown to the receiver.
        #[arg(long)]
        display_name: String,
        /// Offer TTL in seconds.
        #[arg(long, default_value_t = 60)]
        ttl: u64,
        /// Output format: `url`, `json`, or `raw`.
        #[arg(long, default_value = "url")]
        format: String,
    },

    /// Decode an offer-QR payload (= reverse of `offer-qr`).
    /// Accepts an `iddrop://<base64>` URL or a bare base64 string.
    OfferDecode {
        /// QR contents to decode.
        payload: String,
    },

    /// Emit a request for an identity claim (= request-first mode).
    Request {
        /// AINS handle being requested.
        #[arg(long)]
        identity: String,
        /// Receiver's own Ed25519 pubkey (hex, 64 chars).
        #[arg(long)]
        receiver_pubkey: String,
        /// Request TTL in seconds (= default 300s).
        #[arg(long, default_value_t = 300)]
        ttl: u64,
    },

    /// Inspect an iddrop response without accepting it (= metadata only).
    Inspect {
        /// Path to the response JSON (= unsealed iddrop payload).
        path: String,
    },

    /// Validate a response against the six rules (= no materialisation).
    Validate {
        /// Path to the response JSON.
        path: String,
        /// Local Ed25519 pubkey (hex) — the receiver's identity.
        #[arg(long)]
        local_pubkey: String,
        /// Outstanding `request_id` to bind against (request-first).
        #[arg(long)]
        request_id: Option<String>,
        /// Outstanding `challenge_nonce` (hex 32 chars) — required with request-first.
        #[arg(long)]
        challenge_nonce: Option<String>,
        /// Mode hint: `request-first` or `offer-first`.
        #[arg(long, default_value = "request-first")]
        mode: String,
    },

    /// Accept a validated response (= the materialisation step, §8.6).
    /// v0.1 stub: prints "accepted" without persisting.
    Accept {
        /// Path to the validated response JSON.
        path: String,
    },

    /// Send (= POST) an iddrop envelope to a peer's brain-api inbox.
    Send {
        /// Recipient aint, e.g. `jasper.aint` or `jasper`.
        #[arg(long)]
        to: String,
        /// Path to the file containing the sealed envelope bytes.
        path: String,
        /// Override the wire base URL (default `https://brein.jaspervandemeent.nl`).
        #[arg(long, env = "TBZ_IDDROP_URL")]
        url: Option<String>,
        /// Use i-poll transport instead of direct HTTP inbox.
        #[arg(long)]
        via_ipoll: bool,
    },

    /// Pull (= fetch + delete) the next envelope from your inbox.
    /// Writes raw bytes to stdout or to `--output`.
    Pull {
        /// Your aint, e.g. `jasper.aint` or `jasper`.
        #[arg(long)]
        r#as: String,
        /// Write the pulled bytes to this file (default = stdout).
        #[arg(short, long)]
        output: Option<String>,
        /// Override the wire base URL.
        #[arg(long, env = "TBZ_IDDROP_URL")]
        url: Option<String>,
        /// Use i-poll transport instead of direct HTTP inbox.
        #[arg(long)]
        via_ipoll: bool,
    },

    /// Print the spec reference identifier.
    Spec,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Offer {
            identity,
            sender_pubkey,
            display_name,
            ttl,
        } => {
            let offer = Offer::new(sender_pubkey, identity, display_name, ttl);
            println!("{}", serde_json::to_string_pretty(&offer)?);
        }

        Commands::OfferQr {
            identity,
            sender_pubkey,
            display_name,
            ttl,
            format,
        } => {
            use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
            let offer = Offer::new(sender_pubkey, identity, display_name, ttl);
            let json = serde_json::to_vec(&offer)?;
            let b64 = URL_SAFE_NO_PAD.encode(&json);
            match format.as_str() {
                "raw" => println!("{}", b64),
                "json" => println!("{}", serde_json::to_string_pretty(&offer)?),
                "url" | _ => {
                    println!("iddrop://{}", b64);
                    eprintln!();
                    eprintln!("# Render as QR with:");
                    eprintln!("#   iddrop offer-qr ... --format raw | qrencode -t ANSIUTF8");
                    eprintln!("#   iddrop offer-qr ... --format url | qrencode -t PNG -o offer.png");
                }
            }
        }

        Commands::OfferDecode { payload } => {
            use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
            let stripped = payload
                .strip_prefix("iddrop://")
                .unwrap_or(&payload)
                .trim()
                .trim_end_matches('=');
            let bytes = URL_SAFE_NO_PAD
                .decode(stripped)
                .map_err(|e| anyhow::anyhow!("base64 decode: {}", e))?;
            let offer: Offer = serde_json::from_slice(&bytes)?;
            println!("{}", serde_json::to_string_pretty(&offer)?);
        }

        Commands::Request {
            identity,
            receiver_pubkey,
            ttl,
        } => {
            let req = Request::new(identity, "identity", receiver_pubkey, ttl);
            println!("{}", serde_json::to_string_pretty(&req)?);
        }

        Commands::Inspect { path } => {
            let bytes = std::fs::read(&path)?;
            let claim: IdClaim = serde_json::from_slice(&bytes)?;
            println!("iddrop response (inspection only — NOT accepted)");
            println!("  protocol:         {}", claim.protocol);
            println!("  request_id:       {}", claim.request_id);
            println!("  sender_pubkey:    {}", short(&claim.sender_pubkey));
            println!("  receiver_pubkey:  {}", short(&claim.receiver_pubkey));
            println!("  claimed_aint:     {}", claim.claimed_aint);
            println!("  semantic_type:    {:?}", claim.semantic_type);
            println!("  provenance_ref:   {}", claim.provenance_ref);
            println!("  issued_at:        {}", claim.issued_at);
            println!("  expires_at:       {}", claim.expires_at);
            if let Some(parent) = &claim.causal_parent {
                println!("  causal_parent:    {}", parent);
            }
            if !claim.proof_chain.is_empty() {
                println!("  proof_chain:      {} entries", claim.proof_chain.len());
            }
        }

        Commands::Validate {
            path,
            local_pubkey,
            request_id,
            challenge_nonce,
            mode,
        } => {
            let bytes = std::fs::read(&path)?;
            let claim: IdClaim = serde_json::from_slice(&bytes)?;

            // v0.2: pick resolver based on env. If TBZ_AINS_URL is set or
            // the default brain-api endpoint is reachable, use HTTP; else
            // fall back to a single-entry stub mapping claimed_aint →
            // claim.sender_pubkey (= echo, for offline demos).
            let resolver_url = std::env::var("TBZ_AINS_URL")
                .unwrap_or_else(|_| "https://brein.jaspervandemeent.nl".to_string());
            let use_stub = std::env::var("TBZ_AINS_STUB").is_ok();
            let stub = tibet_iddrop::StubAinsResolver::new()
                .with_entry(&claim.claimed_aint, claim.sender_pubkey.clone());
            let http = tibet_iddrop::HttpAinsResolver::new(&resolver_url);
            let resolver: &dyn tibet_iddrop::AinsResolver =
                if use_stub { &stub } else { &http };

            let outstanding: Vec<(String, ChallengeNonce)> =
                match (request_id, challenge_nonce, mode.as_str()) {
                    (Some(rid), Some(nonce_hex), "request-first") => {
                        let n = parse_nonce_hex(&nonce_hex)?;
                        vec![(rid, n)]
                    }
                    _ => Vec::new(),
                };

            let offers: Vec<String> = if mode == "offer-first" {
                vec![claim.request_id.clone()]
            } else {
                Vec::new()
            };

            let ctx = ReceiverContext {
                local_pubkey: &local_pubkey,
                outstanding_requests: &outstanding,
                outstanding_offers: &offers,
                ains_resolver: resolver,
            };

            // Rule 1 (carrier truth) is asserted true for the CLI demo;
            // real callers must run TBZ v2 verify first.
            let outcome = validate(&claim, true, &ctx, &mode);
            match outcome {
                Ok(out) => {
                    println!("✓ validation passed");
                    println!("  mode:            {}", out.mode);
                    println!("  provenance_ref:  {}", out.provenance_ref);
                    println!("  rules passed:");
                    for r in &out.passed {
                        println!("    - {:?}", r);
                    }
                    println!("\n  Reminder: §8.6 Commitment Discipline — validation");
                    println!("  is the PRECONDITION, not the action. Run `iddrop accept`");
                    println!("  explicitly to materialise.");
                }
                Err(e) => {
                    eprintln!("✗ validation failed: {}", e);
                    std::process::exit(1);
                }
            }
        }

        Commands::Accept { path } => {
            let bytes = std::fs::read(&path)?;
            let claim: IdClaim = serde_json::from_slice(&bytes)?;
            println!("iddrop accept (v0.1 stub):");
            println!("  request_id:    {}", claim.request_id);
            println!("  claimed_aint:  {}", claim.claimed_aint);
            println!("  → would persist to local identity register in v0.2");
            println!("\n  NOTE: v0.1 does NOT persist. Run `validate` first; in v0.2");
            println!("  this step will couple to the AInternet AINS register and");
            println!("  the KIT app trust-kernel.");
        }

        Commands::Send { to, path, url, via_ipoll } => {
            use tibet_iddrop::transport::Transport;
            let base = url.unwrap_or_else(|| "https://brein.jaspervandemeent.nl".to_string());
            let bytes = std::fs::read(&path)?;
            let transport: Box<dyn Transport> = if via_ipoll {
                Box::new(tibet_iddrop::transport::wire::IpollTransport::new(&base))
            } else {
                Box::new(tibet_iddrop::transport::wire::HttpTransport::new(&base))
            };
            transport.send(&to, &bytes).map_err(|e| anyhow::anyhow!(e))?;
            println!(
                "iddrop send: {} bytes → {} via {} ({})",
                bytes.len(),
                to,
                transport.name(),
                base
            );
        }

        Commands::Pull { r#as, output, url, via_ipoll } => {
            use tibet_iddrop::transport::Transport;
            let base = url.unwrap_or_else(|| "https://brein.jaspervandemeent.nl".to_string());
            let transport: Box<dyn Transport> = if via_ipoll {
                Box::new(tibet_iddrop::transport::wire::IpollTransport::new(&base))
            } else {
                Box::new(tibet_iddrop::transport::wire::HttpTransport::new(&base))
            };
            match transport.recv(&r#as).map_err(|e| anyhow::anyhow!(e))? {
                Some(bytes) => {
                    if let Some(out) = output {
                        std::fs::write(&out, &bytes)?;
                        eprintln!("iddrop pull: {} bytes → {}", bytes.len(), out);
                    } else {
                        use std::io::Write as _;
                        std::io::stdout().write_all(&bytes)?;
                    }
                }
                None => {
                    eprintln!("iddrop pull: inbox empty");
                    std::process::exit(2);
                }
            }
        }

        Commands::Spec => {
            println!("{}", SPEC_REFERENCE);
            println!("protocol-version: {}", PROTOCOL_VERSION);
            println!("local source:     /srv/jtel-stack/standards/ietf/{}.md", SPEC_REFERENCE);
        }
    }

    Ok(())
}

fn short(hex: &str) -> String {
    if hex.len() <= 20 {
        hex.to_string()
    } else {
        format!("{}…{}", &hex[..10], &hex[hex.len() - 6..])
    }
}

fn parse_nonce_hex(s: &str) -> Result<ChallengeNonce, IddropError> {
    let v = hex::decode(s.trim())
        .map_err(|e| IddropError::Internal(format!("invalid challenge_nonce hex: {}", e)))?;
    if v.len() != 16 {
        return Err(IddropError::Internal(
            "challenge_nonce must be 16 bytes (32 hex chars)".to_string(),
        ));
    }
    let mut out = [0u8; 16];
    out.copy_from_slice(&v);
    Ok(ChallengeNonce(out))
}
