//! Fingerprint-pinned HTTPS (trust-on-first-use, like `p4 trust` / SSH).
//!
//! The webview never talks TLS to the server directly — every request runs
//! through reqwest built with a custom rustls verifier that accepts a
//! connection iff the SHA-256 of the presented leaf certificate equals the
//! pin the user accepted on first connect. No pin -> the connection is
//! refused (`NO_PIN`); wrong cert -> refused (`PIN_MISMATCH`) and the UI
//! hard-stops. Plain http:// URLs (dev servers with disable_tls) bypass
//! pinning and use a default client.

use std::collections::HashMap;
use std::sync::{Arc, LazyLock, Mutex};

use rustls::client::danger::{
    HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use sha2::{Digest, Sha256};

/// "AA:BB:..." SHA-256 fingerprint of a DER certificate — the same format
/// CambiumServer/utilities/tlscert.py prints in the startup banner.
pub fn fingerprint_der(der: &[u8]) -> String {
    let digest = Sha256::digest(der);
    digest
        .iter()
        .map(|b| format!("{b:02X}"))
        .collect::<Vec<_>>()
        .join(":")
}

fn parse_pin(pin: &str) -> Result<Vec<u8>, String> {
    let hex_str: String = pin.chars().filter(|c| *c != ':').collect();
    let bytes = hex::decode(hex_str.to_lowercase()).map_err(|_| "bad pin format".to_string())?;
    if bytes.len() != 32 {
        return Err("bad pin length".into());
    }
    Ok(bytes)
}

/// Real (webpki-backed) handshake-signature verification, shared by both
/// verifiers. The pin/capture logic in `verify_server_cert` only inspects the
/// certificate *bytes*, which are public — any observer of a prior handshake
/// (or of our own trust prompt) has them. Proof that the peer actually
/// possesses the private key comes from checking its signature over the
/// handshake transcript against the presented cert's public key. Returning
/// `HandshakeSignatureValid::assertion()` unconditionally here would let an
/// on-path attacker replay the pinned cert and MITM us despite a matching pin.
fn verify_tls12_sig(
    message: &[u8],
    cert: &CertificateDer<'_>,
    dss: &rustls::DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
    rustls::crypto::verify_tls12_signature(
        message,
        cert,
        dss,
        &rustls::crypto::ring::default_provider().signature_verification_algorithms,
    )
}

fn verify_tls13_sig(
    message: &[u8],
    cert: &CertificateDer<'_>,
    dss: &rustls::DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
    rustls::crypto::verify_tls13_signature(
        message,
        cert,
        dss,
        &rustls::crypto::ring::default_provider().signature_verification_algorithms,
    )
}

/// Accepts iff the leaf cert hashes to the expected pin. Chain, hostname and
/// validity window are deliberately NOT checked: the pin IS the identity
/// (self-signed certs have no chain, and users connect by bare IP). Handshake
/// signatures ARE checked (see `verify_tls12_sig`) — pin match alone is not
/// proof of possession.
#[derive(Debug)]
struct PinVerifier {
    expected: Vec<u8>,
}

impl ServerCertVerifier for PinVerifier {
    fn verify_server_cert(
        &self,
        end_entity: &CertificateDer<'_>,
        _intermediates: &[CertificateDer<'_>],
        _server_name: &ServerName<'_>,
        _ocsp_response: &[u8],
        _now: UnixTime,
    ) -> Result<ServerCertVerified, rustls::Error> {
        let got = Sha256::digest(end_entity.as_ref());
        if got.as_slice() == self.expected.as_slice() {
            Ok(ServerCertVerified::assertion())
        } else {
            Err(rustls::Error::General("PIN_MISMATCH".into()))
        }
    }

    fn verify_tls12_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        verify_tls12_sig(message, cert, dss)
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        verify_tls13_sig(message, cert, dss)
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        rustls::crypto::ring::default_provider()
            .signature_verification_algorithms
            .supported_schemes()
    }
}

/// First-connect inspection: accepts ANY cert but records its DER so the UI
/// can show the fingerprint for the trust prompt. Only ever used for one
/// unauthenticated GET — no credentials ride on this connection. It only
/// bypasses CHAIN validation; handshake signatures are still verified for
/// real, so the fingerprint we show belongs to a peer that proved possession
/// of the matching private key.
#[derive(Debug)]
struct CaptureVerifier {
    seen: Arc<Mutex<Option<Vec<u8>>>>,
}

impl ServerCertVerifier for CaptureVerifier {
    fn verify_server_cert(
        &self,
        end_entity: &CertificateDer<'_>,
        _intermediates: &[CertificateDer<'_>],
        _server_name: &ServerName<'_>,
        _ocsp_response: &[u8],
        _now: UnixTime,
    ) -> Result<ServerCertVerified, rustls::Error> {
        *self.seen.lock().unwrap() = Some(end_entity.as_ref().to_vec());
        Ok(ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        verify_tls12_sig(message, cert, dss)
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &rustls::DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        verify_tls13_sig(message, cert, dss)
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        rustls::crypto::ring::default_provider()
            .signature_verification_algorithms
            .supported_schemes()
    }
}

fn rustls_config_with(verifier: Arc<dyn ServerCertVerifier>) -> rustls::ClientConfig {
    rustls::ClientConfig::builder()
        .dangerous()
        .with_custom_certificate_verifier(verifier)
        .with_no_client_auth()
}

/// The one place clients get built. https + pin -> pinned verifier;
/// https without pin -> refused; http -> plain (dev-only servers).
pub fn build_client(
    url: &str,
    pin: Option<&str>,
    headers: reqwest::header::HeaderMap,
) -> Result<reqwest::Client, String> {
    // Timeouts so a broken/slow transfer fails fast instead of hanging forever
    // (trial A10: a truncated / reset / stalled download otherwise never
    // returns). `read_timeout` is a per-read IDLE timeout — a healthy transfer
    // delivers bytes continuously, so only a genuinely dead connection (no
    // bytes for 30s) trips it, which keeps legitimate large/slow pulls safe.
    let builder = reqwest::Client::builder()
        .default_headers(headers)
        .connect_timeout(std::time::Duration::from_secs(15))
        .read_timeout(std::time::Duration::from_secs(30))
        // Shared clients (shared_client) keep sockets pooled between calls.
        // Idle keep-alives are dropped after 15s: long enough to cover a
        // poll/refresh burst, short enough that a socket half-closed while
        // the laptop slept is discarded instead of poisoning the next send.
        .pool_idle_timeout(std::time::Duration::from_secs(15))
        .pool_max_idle_per_host(4);
    // URL schemes are case-insensitive (RFC 3986 §3.1): "HTTPS://" must hit
    // the pinned branch, not silently fall through to the unpinned client.
    let is_https = url
        .get(.."https://".len())
        .is_some_and(|p| p.eq_ignore_ascii_case("https://"));
    if is_https {
        let pin = pin.ok_or_else(|| "NO_PIN".to_string())?;
        let verifier = Arc::new(PinVerifier { expected: parse_pin(pin)? });
        builder
            .use_preconfigured_tls(rustls_config_with(verifier))
            .build()
            .map_err(|e| e.to_string())
    } else {
        builder.build().map_err(|e| e.to_string())
    }
}

/// One pooled client per (origin, pin): connection reuse (keep-alive) across
/// requests to the same server identity, instead of a fresh TCP+TLS
/// handshake per call. Auth headers are
/// deliberately NOT part of the key — they ride per-request — so a token
/// refresh keeps the warm pool. A re-trusted host changes the pin and thus
/// the key; the superseded entry is a few KB and is left to die with the
/// process.
type ClientKey = (String, Option<String>);
static CLIENT_CACHE: LazyLock<Mutex<HashMap<ClientKey, reqwest::Client>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

/// scheme://host:port with the scheme-default port made explicit, so
/// "https://h/x" and "https://h:443/y" share one client.
fn origin_of(url: &str) -> Result<String, String> {
    let parsed = reqwest::Url::parse(url).map_err(|e| format!("bad url: {e}"))?;
    let host = parsed.host_str().ok_or_else(|| "bad url: no host".to_string())?;
    let port = parsed.port_or_known_default().unwrap_or(0);
    Ok(format!("{}://{host}:{port}", parsed.scheme()))
}

pub fn shared_client(url: &str, pin: Option<&str>) -> Result<reqwest::Client, String> {
    let key = (origin_of(url)?, pin.map(str::to_string));
    if let Some(client) = CLIENT_CACHE.lock().unwrap().get(&key) {
        return Ok(client.clone());
    }
    // Built outside the lock: rustls config construction is not free and must
    // not serialize unrelated hosts. A racing double-build is harmless — last
    // insert wins and both clients work.
    let client = build_client(url, pin, reqwest::header::HeaderMap::new())?;
    CLIENT_CACHE.lock().unwrap().insert(key, client.clone());
    Ok(client)
}

fn map_reqwest_error(err: reqwest::Error) -> String {
    // rustls errors arrive wrapped several layers deep; surface our sentinel.
    let text = format!("{err:?}");
    if text.contains("PIN_MISMATCH") {
        "PIN_MISMATCH".to_string()
    } else {
        err.to_string()
    }
}

#[derive(Debug, serde::Serialize)]
pub struct FetchResponse {
    pub status: u16,
    pub body: String,
}

pub async fn pinned_fetch(
    method: String,
    url: String,
    headers: HashMap<String, String>,
    body: Option<Vec<u8>>,
    pin: Option<String>,
    binary: Option<bool>,
) -> Result<FetchResponse, String> {
    let header_map = crate::transfer::build_headers(&headers);
    let client = shared_client(&url, pin.as_deref())?;
    let method = reqwest::Method::from_bytes(method.as_bytes()).map_err(|e| e.to_string())?;
    let mut req = client.request(method, &url).headers(header_map);
    if let Some(bytes) = body {
        req = req.body(bytes);
    }
    let resp = req.send().await.map_err(map_reqwest_error)?;
    let status = resp.status().as_u16();
    // Binary responses (avatars) are base64-encoded so they survive the JSON
    // round-trip back to JS; text responses (the JSON API) pass through as-is.
    let body = if binary.unwrap_or(false) {
        use base64::Engine as _;
        let bytes = resp.bytes().await.map_err(map_reqwest_error)?;
        base64::engine::general_purpose::STANDARD.encode(&bytes)
    } else {
        resp.text().await.map_err(map_reqwest_error)?
    };
    Ok(FetchResponse { status, body })
}

#[derive(serde::Serialize)]
pub struct CertInfo {
    pub fingerprint: String,
    pub subject: String,
    pub not_before: String,
    pub not_after: String,
}

pub async fn fetch_server_certificate(host: String) -> Result<CertInfo, String> {
    let seen = Arc::new(Mutex::new(None));
    let verifier = Arc::new(CaptureVerifier { seen: seen.clone() });
    let client = reqwest::Client::builder()
        .use_preconfigured_tls(rustls_config_with(verifier))
        .build()
        .map_err(|e| e.to_string())?;
    // Unauthenticated endpoint; we only need the handshake to complete.
    let url = format!("https://{host}/api/server");
    client.get(&url).send().await.map_err(|e| e.to_string())?;
    let der = seen
        .lock()
        .unwrap()
        .take()
        .ok_or_else(|| "no certificate captured".to_string())?;
    let (_, cert) = x509_parser::parse_x509_certificate(&der).map_err(|e| e.to_string())?;
    Ok(CertInfo {
        fingerprint: fingerprint_der(&der),
        subject: cert.subject().to_string(),
        not_before: cert.validity().not_before.to_string(),
        not_after: cert.validity().not_after.to_string(),
    })
}

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

    #[test]
    fn fingerprint_matches_python_format() {
        // sha256(b"hello") = 2CF24DBA...
        let fp = fingerprint_der(b"hello");
        assert!(fp.starts_with("2C:F2:4D:BA"));
        assert_eq!(fp.split(':').count(), 32);
        assert!(fp.chars().all(|c| c.is_ascii_hexdigit() || c == ':'));
    }

    #[test]
    fn parse_pin_roundtrip() {
        let fp = fingerprint_der(b"hello");
        let bytes = parse_pin(&fp).unwrap();
        assert_eq!(bytes.as_slice(), Sha256::digest(b"hello").as_slice());
    }

    #[test]
    fn parse_pin_rejects_garbage() {
        assert!(parse_pin("not-a-pin").is_err());
        assert!(parse_pin("AA:BB").is_err()); // too short
    }

    #[test]
    fn https_without_pin_refused() {
        let headers = reqwest::header::HeaderMap::new();
        let err = build_client("https://example.com", None, headers).unwrap_err();
        assert_eq!(err, "NO_PIN");
    }

    #[test]
    fn uppercase_https_scheme_still_requires_pin() {
        // Scheme comparison must be case-insensitive: "HTTPS://" may not
        // fall through to the unpinned plain-http branch.
        let headers = reqwest::header::HeaderMap::new();
        let err = build_client("HTTPS://example.com", None, headers).unwrap_err();
        assert_eq!(err, "NO_PIN");
        let headers = reqwest::header::HeaderMap::new();
        let err = build_client("HttpS://example.com", None, headers).unwrap_err();
        assert_eq!(err, "NO_PIN");
    }

    #[test]
    fn http_without_pin_allowed() {
        let headers = reqwest::header::HeaderMap::new();
        assert!(build_client("http://localhost:8666", None, headers).is_ok());
    }

    #[test]
    fn pin_verifier_accepts_matching_cert() {
        let der = b"fake-cert-der".to_vec();
        let verifier = PinVerifier { expected: Sha256::digest(&der).to_vec() };
        let cert = CertificateDer::from(der.clone());
        let name = ServerName::try_from("localhost").unwrap();
        let ok = verifier.verify_server_cert(
            &cert, &[], &name, &[], UnixTime::now(),
        );
        assert!(ok.is_ok());
    }

    #[test]
    fn pin_verifier_rejects_wrong_cert() {
        let verifier = PinVerifier { expected: Sha256::digest(b"expected").to_vec() };
        let cert = CertificateDer::from(b"different".to_vec());
        let name = ServerName::try_from("localhost").unwrap();
        let err = verifier
            .verify_server_cert(&cert, &[], &name, &[], UnixTime::now())
            .unwrap_err();
        assert!(format!("{err:?}").contains("PIN_MISMATCH"));
    }

    #[test]
    fn shared_client_caches_per_origin_and_pin() {
        let pin = "00".repeat(32); // parse_pin: 32 bytes of hex, colons optional
        let key_a = ("https://cache-test-a.invalid:8666".to_string(), Some(pin.clone()));
        assert!(!CLIENT_CACHE.lock().unwrap().contains_key(&key_a));
        shared_client("https://cache-test-a.invalid:8666/api/x", Some(&pin)).unwrap();
        assert!(CLIENT_CACHE.lock().unwrap().contains_key(&key_a));
        // Same origin + pin, different path: reuses the entry (still exactly one).
        shared_client("https://cache-test-a.invalid:8666/api/y", Some(&pin)).unwrap();
        assert!(CLIENT_CACHE.lock().unwrap().contains_key(&key_a));
        // Same origin, no pin (plain http): a distinct identity, distinct entry.
        let key_b = ("http://cache-test-b.invalid:8666".to_string(), None);
        shared_client("http://cache-test-b.invalid:8666/", None).unwrap();
        assert!(CLIENT_CACHE.lock().unwrap().contains_key(&key_b));
    }

    #[test]
    fn shared_client_still_refuses_pinless_https() {
        let err = shared_client("https://cache-test-c.invalid:1/", None).unwrap_err();
        assert_eq!(err, "NO_PIN");
    }

    #[test]
    fn origin_of_normalizes_default_ports() {
        assert_eq!(origin_of("https://h.invalid/api/x").unwrap(), "https://h.invalid:443");
        assert_eq!(origin_of("http://h.invalid:8666/y?z=1").unwrap(), "http://h.invalid:8666");
        assert!(origin_of("not a url").is_err());
    }
}

/// Real-socket TLS tests: a one-shot rustls server on a loopback port, hit
/// through the full `pinned_fetch` path (reqwest + preconfigured rustls).
/// These prove three things unit tests can't:
///  - the `use_preconfigured_tls` downcast works against the linked rustls,
///  - the `PIN_MISMATCH` sentinel survives reqwest's error nesting
///    (`map_reqwest_error`'s substring match),
///  - a peer that presents the pinned certificate but does NOT hold its
///    private key is rejected (proof-of-possession via handshake signature).
#[cfg(test)]
mod tls_e2e_tests {
    use super::*;
    use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
    use std::io::{Read, Write};
    use std::net::TcpListener;

    struct TestIdentity {
        cert_der: Vec<u8>,
        key_der: Vec<u8>,
    }

    fn gen_identity() -> TestIdentity {
        let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
        TestIdentity {
            cert_der: ck.cert.der().to_vec(),
            key_der: ck.key_pair.serialize_der(),
        }
    }

    /// Serve exactly one TLS connection, presenting `cert_der` and signing
    /// the handshake with `key_der`. The two are deliberately allowed to
    /// mismatch (via a raw cert resolver, which skips the consistency check
    /// `with_single_cert` would perform) so a test can impersonate "attacker
    /// replays the pinned cert without its private key".
    fn spawn_one_shot_tls_server(
        cert_der: Vec<u8>,
        key_der: Vec<u8>,
    ) -> (u16, std::thread::JoinHandle<()>) {
        #[derive(Debug)]
        struct FixedCert(Arc<rustls::sign::CertifiedKey>);
        impl rustls::server::ResolvesServerCert for FixedCert {
            fn resolve(
                &self,
                _hello: rustls::server::ClientHello<'_>,
            ) -> Option<Arc<rustls::sign::CertifiedKey>> {
                Some(self.0.clone())
            }
        }

        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let port = listener.local_addr().unwrap().port();
        let handle = std::thread::spawn(move || {
            let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_der));
            let signing_key = rustls::crypto::ring::sign::any_supported_type(&key).unwrap();
            let certified = Arc::new(rustls::sign::CertifiedKey::new(
                vec![CertificateDer::from(cert_der)],
                signing_key,
            ));
            let config = rustls::ServerConfig::builder_with_provider(Arc::new(
                rustls::crypto::ring::default_provider(),
            ))
            .with_safe_default_protocol_versions()
            .unwrap()
            .with_no_client_auth()
            .with_cert_resolver(Arc::new(FixedCert(certified)));

            let (mut stream, _) = listener.accept().unwrap();
            let mut conn = rustls::ServerConnection::new(Arc::new(config)).unwrap();
            let mut tls = rustls::Stream::new(&mut conn, &mut stream);
            // Read until end of request headers; any error just means the
            // client (correctly) aborted the handshake.
            let mut buf = [0u8; 4096];
            let mut req = Vec::new();
            loop {
                match tls.read(&mut buf) {
                    Ok(0) | Err(_) => return,
                    Ok(n) => {
                        req.extend_from_slice(&buf[..n]);
                        if req.windows(4).any(|w| w == b"\r\n\r\n") {
                            break;
                        }
                    }
                }
            }
            let _ = tls.write_all(
                b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\nconnection: close\r\n\r\nok",
            );
            let _ = tls.flush();
        });
        (port, handle)
    }

    async fn fetch(port: u16, pin: &str) -> Result<FetchResponse, String> {
        pinned_fetch(
            "GET".into(),
            format!("https://127.0.0.1:{port}/"),
            HashMap::new(),
            None,
            Some(pin.to_string()),
            None,
        )
        .await
    }

    #[tokio::test]
    async fn pinned_fetch_succeeds_against_matching_cert_and_key() {
        let id = gen_identity();
        let pin = fingerprint_der(&id.cert_der);
        let (port, server) = spawn_one_shot_tls_server(id.cert_der.clone(), id.key_der);
        let resp = fetch(port, &pin).await.expect("pinned fetch should succeed");
        assert_eq!(resp.status, 200);
        assert_eq!(resp.body, "ok");
        server.join().unwrap();
    }

    #[tokio::test]
    async fn pinned_fetch_binary_flag_base64_encodes_the_body() {
        // Avatars: binary:true must return the raw bytes base64-encoded
        // rather than lossily decoded as UTF-8 text.
        let id = gen_identity();
        let pin = fingerprint_der(&id.cert_der);
        let (port, server) = spawn_one_shot_tls_server(id.cert_der.clone(), id.key_der);
        let resp = pinned_fetch(
            "GET".into(),
            format!("https://127.0.0.1:{port}/"),
            HashMap::new(),
            None,
            Some(pin),
            Some(true),
        )
        .await
        .expect("pinned fetch should succeed");
        assert_eq!(resp.status, 200);
        use base64::Engine as _;
        assert_eq!(resp.body, base64::engine::general_purpose::STANDARD.encode(b"ok"));
        server.join().unwrap();
    }

    #[tokio::test]
    async fn pinned_fetch_wrong_cert_yields_exact_pin_mismatch_sentinel() {
        // Server presents cert B (with its own key — a fully valid identity),
        // but the client pinned cert A: the error must be the exact sentinel,
        // proving map_reqwest_error unwraps the nested rustls error.
        let pinned = gen_identity();
        let other = gen_identity();
        let pin = fingerprint_der(&pinned.cert_der);
        let (port, server) = spawn_one_shot_tls_server(other.cert_der, other.key_der);
        let err = fetch(port, &pin).await.expect_err("wrong cert must be refused");
        assert_eq!(err, "PIN_MISMATCH");
        server.join().unwrap();
    }

    #[tokio::test]
    async fn replayed_pinned_cert_without_its_key_is_rejected() {
        // Attacker scenario: the peer sends the pinned certificate's public
        // bytes (which any handshake observer has) but signs the transcript
        // with a DIFFERENT private key. The pin check alone would pass — the
        // handshake-signature check must reject the connection.
        let pinned = gen_identity();
        let attacker = gen_identity();
        let pin = fingerprint_der(&pinned.cert_der);
        let (port, server) = spawn_one_shot_tls_server(pinned.cert_der, attacker.key_der);
        let result = fetch(port, &pin).await;
        let err = result
            .map(|r| format!("accepted with status {}", r.status))
            .expect_err("replayed cert without its private key MUST be rejected");
        // Not a pin failure — the pin matched; possession proof failed.
        assert_ne!(err, "PIN_MISMATCH", "expected a signature failure, got: {err}");
        server.join().unwrap();
    }
}
