use std::path::Path;
use std::sync::Arc;

use hmac::{Hmac, Mac};
use sha2::Sha256;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::sync::Mutex;

use wirken_ipc::wirken_capnp::frame;
use wirken_ipc::{
    AdapterIdentity, IpcFrameReader, IpcFrameWriter, connect, perform_adapter_handshake,
    split_stream,
};

use wirken_adapter_core::approval::{self, ApprovalPayload, Decision};

use crate::convert::{self, ApprovalPress, WhatsAppInbound};
use crate::error::WhatsAppError;

type HmacSha256 = Hmac<Sha256>;

/// WhatsApp Cloud API adapter: bridges Meta's webhook API <-> Wirken gateway IPC.
pub struct WhatsAppAdapter {
    identity: AdapterIdentity,
    access_token: String,
    phone_number_id: String,
    verify_token: String,
    app_secret: String,
    listen_port: u16,
}

impl WhatsAppAdapter {
    pub fn new(
        identity: AdapterIdentity,
        access_token: String,
        phone_number_id: String,
        verify_token: String,
        app_secret: String,
        listen_port: u16,
    ) -> Result<Self, WhatsAppError> {
        // Refuse to start without an app secret. A missing secret used
        // to silently disable webhook HMAC verification, turning the
        // adapter into an unauthenticated POST endpoint. Fail closed
        // at construction so no runtime path reaches the handler
        // without a configured secret.
        if app_secret.is_empty() {
            return Err(WhatsAppError::Config(
                "WhatsApp adapter requires a non-empty app_secret; \
                 webhook signature verification is mandatory"
                    .into(),
            ));
        }
        Ok(Self {
            identity,
            access_token,
            phone_number_id,
            verify_token,
            app_secret,
            listen_port,
        })
    }

    /// Connect to gateway, authenticate, then run the webhook listener.
    pub async fn run(&self, socket_path: &Path) -> Result<(), WhatsAppError> {
        tracing::info!("Connecting to gateway at {}", socket_path.display());
        let stream = connect(socket_path).await?;
        let (mut reader, mut writer) = split_stream(stream);

        tracing::info!("Performing handshake as '{}'", self.identity.adapter_id());
        perform_adapter_handshake(&mut reader, &mut writer, &self.identity).await?;
        tracing::info!("Handshake complete");

        let writer = Arc::new(Mutex::new(writer));

        // Spawn outbound handler
        let out_token = self.access_token.clone();
        let out_phone_id = self.phone_number_id.clone();
        let out_writer = writer.clone();
        let _outbound_handle = tokio::spawn(async move {
            handle_outbound(reader, out_token, out_phone_id, out_writer).await;
        });

        // Spawn heartbeat
        let hb_writer = writer.clone();
        let _hb_handle = tokio::spawn(async move {
            heartbeat_loop(hb_writer).await;
        });

        // Run HTTP webhook listener
        tracing::info!(
            "Starting WhatsApp webhook listener on port {}",
            self.listen_port
        );
        let listener = TcpListener::bind(format!("127.0.0.1:{}", self.listen_port)).await?;

        let verify_token = self.verify_token.clone();
        let app_secret = self.app_secret.clone();

        loop {
            let (mut stream, _) = listener.accept().await?;
            let w = writer.clone();
            let vt = verify_token.clone();
            let secret = app_secret.clone();

            tokio::spawn(async move {
                if let Err(e) = handle_webhook(&mut stream, &vt, &secret, w).await {
                    tracing::error!("Webhook error: {e}");
                }
            });
        }
    }
}

/// Handle an incoming webhook request from Meta.
async fn handle_webhook(
    stream: &mut tokio::net::TcpStream,
    verify_token: &str,
    app_secret: &str,
    writer: Arc<Mutex<IpcFrameWriter>>,
) -> Result<(), WhatsAppError> {
    let mut buf = vec![0u8; 65536];
    let n = stream
        .read(&mut buf)
        .await
        .map_err(|e| WhatsAppError::Webhook(e.to_string()))?;
    if n == 0 {
        return Ok(());
    }

    let request = String::from_utf8_lossy(&buf[..n]);
    let first_line = request.lines().next().unwrap_or("");

    // Webhook verification (GET request from Meta)
    if first_line.starts_with("GET /webhook") {
        return handle_verification(stream, &request, verify_token).await;
    }

    // Webhook event (POST request)
    if first_line.starts_with("POST /webhook") {
        let body = request.split("\r\n\r\n").nth(1).unwrap_or("");

        // HMAC signature is required. The constructor guarantees
        // app_secret is non-empty, so this path always enforces
        // verification and rejects unsigned or mismatched requests.
        let signature = extract_header(&request, "X-Hub-Signature-256");
        if !verify_signature(app_secret, body, &signature) {
            let resp = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
            let _ = stream.write_all(resp.as_bytes()).await;
            return Ok(());
        }

        let json: serde_json::Value = serde_json::from_str(body).unwrap_or_default();

        // Two extractors over the same payload, two output types:
        // text messages route through `frame::Inbound`, button
        // presses through `frame::ApprovalDecision`. Folding both
        // into a single function would mix unrelated domain
        // objects through one return type.
        if let Some(messages) = extract_messages(&json) {
            for msg in messages {
                if !convert::should_process(&msg) {
                    continue;
                }
                let mut capnp_msg = capnp::message::Builder::new_default();
                convert::whatsapp_to_inbound(&msg, &mut capnp_msg);
                let mut w = writer.lock().await;
                if let Err(e) = w.write_message(&capnp_msg).await {
                    tracing::error!("Failed to forward to gateway: {e}");
                }
            }
        }
        for press in extract_button_replies(&json) {
            forward_approval_decision(&press, &writer).await;
        }

        let resp = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
        let _ = stream.write_all(resp.as_bytes()).await;
    } else {
        let resp = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
        let _ = stream.write_all(resp.as_bytes()).await;
    }

    Ok(())
}

/// Handle Meta's webhook verification challenge.
async fn handle_verification(
    stream: &mut tokio::net::TcpStream,
    request: &str,
    verify_token: &str,
) -> Result<(), WhatsAppError> {
    let first_line = request.lines().next().unwrap_or("");
    let query = first_line
        .split_whitespace()
        .nth(1)
        .and_then(|path| path.split('?').nth(1))
        .unwrap_or("");

    let params: std::collections::HashMap<&str, &str> =
        query.split('&').filter_map(|p| p.split_once('=')).collect();

    let mode = params.get("hub.mode").copied().unwrap_or("");
    let token = params.get("hub.verify_token").copied().unwrap_or("");
    let challenge = params.get("hub.challenge").copied().unwrap_or("");

    // The verify_token is a long-lived shared secret Meta echoes back
    // in `?hub.verify_token=`. A `==` short-circuits at the first
    // mismatched byte and leaks token length / common-prefix timing
    // to a tunnel-front attacker; constant-time compare on equal-
    // length byte slices closes that side channel.
    let token_matches = if token.len() == verify_token.len() {
        use subtle::ConstantTimeEq;
        token.as_bytes().ct_eq(verify_token.as_bytes()).into()
    } else {
        false
    };
    if mode == "subscribe" && token_matches {
        let resp = format!(
            "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
            challenge.len(),
            challenge
        );
        let _ = stream.write_all(resp.as_bytes()).await;
    } else {
        let resp = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
        let _ = stream.write_all(resp.as_bytes()).await;
    }

    Ok(())
}

/// Extract messages from the WhatsApp Cloud API webhook payload.
pub(crate) fn extract_messages(json: &serde_json::Value) -> Option<Vec<WhatsAppInbound>> {
    let entry = json.get("entry")?.as_array()?.first()?;
    let changes = entry.get("changes")?.as_array()?;

    let mut messages = Vec::new();

    for change in changes {
        let value = change.get("value")?;
        let phone_number_id = value
            .get("metadata")
            .and_then(|m| m.get("phone_number_id"))
            .and_then(|p| p.as_str())
            .unwrap_or("")
            .to_string();

        let contacts = value.get("contacts").and_then(|c| c.as_array());

        if let Some(msgs) = value.get("messages").and_then(|m| m.as_array()) {
            for msg in msgs {
                let msg_type = msg.get("type").and_then(|t| t.as_str()).unwrap_or("");
                if msg_type != "text" {
                    continue;
                }

                let from = msg
                    .get("from")
                    .and_then(|f| f.as_str())
                    .unwrap_or("")
                    .to_string();

                let from_name = contacts
                    .and_then(|c| {
                        c.iter().find_map(|contact| {
                            let wa_id = contact.get("wa_id").and_then(|w| w.as_str()).unwrap_or("");
                            if wa_id == from {
                                contact
                                    .get("profile")
                                    .and_then(|p| p.get("name"))
                                    .and_then(|n| n.as_str())
                                    .map(|s| s.to_string())
                            } else {
                                None
                            }
                        })
                    })
                    .unwrap_or_else(|| from.clone());

                let text = msg
                    .get("text")
                    .and_then(|t| t.get("body"))
                    .and_then(|b| b.as_str())
                    .unwrap_or("")
                    .to_string();

                let message_id = msg
                    .get("id")
                    .and_then(|i| i.as_str())
                    .unwrap_or("")
                    .to_string();

                let timestamp = msg
                    .get("timestamp")
                    .and_then(|t| t.as_str())
                    .and_then(|t| t.parse::<i64>().ok())
                    .unwrap_or(0);

                messages.push(WhatsAppInbound {
                    message_id,
                    from,
                    from_name,
                    text,
                    timestamp,
                    phone_number_id: phone_number_id.clone(),
                });
            }
        }
    }

    Some(messages)
}

/// Extract button-press approval interactions from a Meta webhook
/// payload. Sibling to [`extract_messages`]: same JSON walk shape
/// but a different output type. Returns an empty vec on payloads
/// with no `interactive`/`button_reply` entries.
///
/// Filters out interactive messages whose subtype is not
/// `button_reply` (e.g. `list_reply`); those are out of scope for
/// the approval flow.
pub(crate) fn extract_button_replies(json: &serde_json::Value) -> Vec<ApprovalPress> {
    let mut out = Vec::new();
    let Some(entry) = json
        .get("entry")
        .and_then(|e| e.as_array())
        .and_then(|a| a.first())
    else {
        return out;
    };
    let Some(changes) = entry.get("changes").and_then(|c| c.as_array()) else {
        return out;
    };
    for change in changes {
        let Some(value) = change.get("value") else {
            continue;
        };
        let contacts = value.get("contacts").and_then(|c| c.as_array());
        let Some(messages) = value.get("messages").and_then(|m| m.as_array()) else {
            continue;
        };
        for msg in messages {
            if msg.get("type").and_then(|t| t.as_str()) != Some("interactive") {
                continue;
            }
            let Some(interactive) = msg.get("interactive") else {
                continue;
            };
            if interactive.get("type").and_then(|t| t.as_str()) != Some("button_reply") {
                continue;
            }
            let Some(button) = interactive.get("button_reply") else {
                continue;
            };
            let Some(encoded) = button.get("id").and_then(|i| i.as_str()) else {
                continue;
            };
            let from = msg
                .get("from")
                .and_then(|f| f.as_str())
                .unwrap_or("")
                .to_string();
            if from.is_empty() {
                continue;
            }
            let from_name = contacts
                .and_then(|c| {
                    c.iter().find_map(|contact| {
                        let wa_id = contact.get("wa_id").and_then(|w| w.as_str()).unwrap_or("");
                        if wa_id == from {
                            contact
                                .get("profile")
                                .and_then(|p| p.get("name"))
                                .and_then(|n| n.as_str())
                                .map(|s| s.to_string())
                        } else {
                            None
                        }
                    })
                })
                .unwrap_or_else(|| from.clone());
            let message_id = msg
                .get("id")
                .and_then(|i| i.as_str())
                .unwrap_or("")
                .to_string();
            out.push(ApprovalPress {
                from,
                from_name,
                encoded_payload: encoded.to_string(),
                message_id,
            });
        }
    }
    out
}

/// Decode an extracted press and forward an `ApprovalDecision`
/// frame to the gateway. Drops the press on the malformed-payload
/// path with a warn log; the 200 OK has already gone back to Meta
/// by the time this runs, so the webhook is acknowledged either
/// way. Mirrors Telegram / Discord / Slack / Teams's posture for
/// malformed input.
async fn forward_approval_decision(press: &ApprovalPress, writer: &Arc<Mutex<IpcFrameWriter>>) {
    let payload = match approval::decode(&press.encoded_payload) {
        Ok(p) => p,
        Err(e) => {
            tracing::warn!(
                from = %press.from,
                button_id = %press.encoded_payload,
                error = %e,
                "whatsapp interaction: unrecognized button_reply.id; dropping"
            );
            return;
        }
    };
    let is_allow = matches!(payload.decision, Decision::Allow);
    let mut capnp_msg = capnp::message::Builder::new_default();
    convert::build_approval_decision(
        &mut capnp_msg,
        &payload.request_id,
        is_allow,
        &press.from,
        &press.from_name,
    );
    let mut w = writer.lock().await;
    if let Err(e) = w.write_message(&capnp_msg).await {
        tracing::error!("whatsapp interaction: failed to send ApprovalDecision to gateway: {e}");
    }
}

/// Handle outbound messages from gateway to WhatsApp.
async fn handle_outbound(
    mut reader: IpcFrameReader,
    access_token: String,
    phone_number_id: String,
    writer: Arc<Mutex<IpcFrameWriter>>,
) {
    let http = reqwest::Client::new();

    loop {
        let msg = match reader.read_message().await {
            Ok(msg) => msg,
            Err(wirken_ipc::IpcError::ConnectionClosed) => {
                tracing::info!("Gateway connection closed");
                break;
            }
            Err(e) => {
                tracing::error!("IPC read error: {e}");
                break;
            }
        };

        let action = {
            let frame_reader = match msg.get_root::<frame::Reader<'_>>() {
                Ok(r) => r,
                Err(e) => {
                    tracing::error!("Failed to parse frame: {e}");
                    continue;
                }
            };

            match frame_reader.which() {
                Ok(frame::Outbound(_)) => match convert::parse_outbound(&msg) {
                    Ok(fields) => FrameAction::SendMessage(fields),
                    Err(e) => {
                        tracing::error!("Failed to parse outbound: {e}");
                        FrameAction::Skip
                    }
                },
                Ok(frame::ApprovalRequest(_)) => match convert::parse_approval_request(&msg) {
                    Ok(fields) => FrameAction::SendApprovalRequest(fields),
                    Err(e) => {
                        tracing::error!("Failed to parse approval request: {e}");
                        FrameAction::Skip
                    }
                },
                Ok(frame::Heartbeat(_)) => FrameAction::Skip,
                Ok(_) => FrameAction::Skip,
                Err(e) => {
                    tracing::error!("Frame variant error: {e}");
                    FrameAction::Skip
                }
            }
        };

        match action {
            FrameAction::SendMessage(fields) => {
                // phone_number_id is a public Meta-assigned identifier, not sensitive.
                // All requests go over HTTPS to graph.facebook.com.
                let url = format!(
                    "https://graph.facebook.com/v21.0/{}/messages",
                    phone_number_id
                );

                let body = serde_json::json!({
                    "messaging_product": "whatsapp",
                    "to": fields.conversation_id,
                    "type": "text",
                    "text": {
                        "body": fields.text,
                    }
                });

                let resp = http
                    .post(&url)
                    .header("Authorization", format!("Bearer {access_token}"))
                    .json(&body)
                    .send()
                    .await;

                let (success, msg_id, error) = match resp {
                    Ok(r) if r.status().is_success() => {
                        let body: serde_json::Value = r.json().await.unwrap_or_default();
                        let mid = body["messages"]
                            .as_array()
                            .and_then(|a| a.first())
                            .and_then(|m| m["id"].as_str())
                            .unwrap_or("")
                            .to_string();
                        (true, mid, String::new())
                    }
                    Ok(r) => {
                        let body = r.text().await.unwrap_or_default();
                        (false, String::new(), body)
                    }
                    Err(e) => (false, String::new(), e.to_string()),
                };

                let mut result_msg = capnp::message::Builder::new_default();
                convert::build_outbound_result(&mut result_msg, success, &msg_id, &error);
                let mut w = writer.lock().await;
                if let Err(e) = w.write_message(&result_msg).await {
                    tracing::error!("Failed to send outbound result: {e}");
                }
            }
            FrameAction::SendApprovalRequest(fields) => {
                send_approval_request(&http, &access_token, &phone_number_id, fields, &writer)
                    .await;
            }
            FrameAction::Skip => {}
        }
    }
}

enum FrameAction {
    SendMessage(convert::OutboundFields),
    SendApprovalRequest(convert::ApprovalRequestFields),
    Skip,
}

/// Render an approval request as a WhatsApp interactive button
/// message and POST it to the Cloud API. Each button's `id` carries
/// the cross-adapter encoded payload from
/// `wirken_adapter_core::approval`; the inbound press path reads it
/// back via `extract_button_replies`.
///
/// On send failure, emits an `ApprovalRequestFailed` frame. The
/// failure reason is classified by `classify_send_error` against
/// Meta's documented error taxonomy; the 24-hour-window-closed
/// case maps to `"window_closed"` so the gateway-side audit row
/// distinguishes "send refused by platform" from "network broken"
/// or "malformed request".
async fn send_approval_request(
    http: &reqwest::Client,
    access_token: &str,
    phone_number_id: &str,
    fields: convert::ApprovalRequestFields,
    writer: &Arc<Mutex<IpcFrameWriter>>,
) {
    let allow_id = match approval::encode(&ApprovalPayload {
        request_id: fields.request_id.clone(),
        decision: Decision::Allow,
    }) {
        Ok(s) => s,
        Err(e) => {
            tracing::error!(
                request_id = %fields.request_id,
                error = %e,
                "whatsapp approval: encode failed; emitting ApprovalRequestFailed"
            );
            emit_approval_failure(writer, &fields.request_id, "encode_failed").await;
            return;
        }
    };
    let deny_id = match approval::encode(&ApprovalPayload {
        request_id: fields.request_id.clone(),
        decision: Decision::Deny,
    }) {
        Ok(s) => s,
        Err(e) => {
            tracing::error!(
                request_id = %fields.request_id,
                error = %e,
                "whatsapp approval: encode failed; emitting ApprovalRequestFailed"
            );
            emit_approval_failure(writer, &fields.request_id, "encode_failed").await;
            return;
        }
    };

    let prompt = format!(
        "Agent {} requests {} (tier {}).\nAction: {}\nTrigger: {}",
        fields.triggering_agent,
        fields.tool_name,
        fields.requested_tier,
        fields.action_key,
        if fields.trigger_message.is_empty() {
            "(none)".to_string()
        } else {
            fields.trigger_message.clone()
        },
    );

    let body = serde_json::json!({
        "messaging_product": "whatsapp",
        "to": fields.target_channel_id,
        "type": "interactive",
        "interactive": {
            "type": "button",
            "body": { "text": prompt },
            "action": {
                "buttons": [
                    {
                        "type": "reply",
                        "reply": { "id": allow_id, "title": "Approve" }
                    },
                    {
                        "type": "reply",
                        "reply": { "id": deny_id, "title": "Deny" }
                    }
                ]
            }
        }
    });

    let url = format!("https://graph.facebook.com/v21.0/{phone_number_id}/messages");
    let resp = http
        .post(&url)
        .header("Authorization", format!("Bearer {access_token}"))
        .json(&body)
        .send()
        .await;

    match resp {
        Ok(r) if r.status().is_success() => {}
        Ok(r) => {
            let status = r.status();
            let response_body = r.text().await.unwrap_or_default();
            let reason = classify_send_error(&response_body);
            tracing::error!(
                request_id = %fields.request_id,
                channel_id = %fields.target_channel_id,
                status = status.as_u16(),
                body = %response_body,
                reason = %reason,
                "whatsapp approval: send failed; emitting ApprovalRequestFailed"
            );
            emit_approval_failure(writer, &fields.request_id, reason).await;
        }
        Err(e) => {
            tracing::error!(
                request_id = %fields.request_id,
                error = %e,
                "whatsapp approval: network error; emitting ApprovalRequestFailed"
            );
            emit_approval_failure(writer, &fields.request_id, "network_error").await;
        }
    }
}

async fn emit_approval_failure(
    writer: &Arc<Mutex<IpcFrameWriter>>,
    request_id: &str,
    reason: &str,
) {
    let mut failure = capnp::message::Builder::new_default();
    convert::build_approval_request_failed(&mut failure, request_id, reason);
    let mut w = writer.lock().await;
    if let Err(send_err) = w.write_message(&failure).await {
        tracing::error!("whatsapp approval: failed to send ApprovalRequestFailed: {send_err}");
    }
}

/// Classify a Meta Cloud API error response body into a stable
/// snake_case `reason` label for `ApprovalRequestFailed`.
///
/// The body is JSON shaped like:
/// `{"error":{"message":"...","code":131047,"type":"OAuthException",...}}`.
///
/// The 24-hour-window-closed case is the load-bearing classification
/// for this slice: Meta's documented codes for "more than 24 hours
/// since recipient's last inbound" are `131047` and `131026`. These
/// codes come from the Meta error-taxonomy reference. The exact set
/// of codes Meta returns for window-closed has shifted historically;
/// re-verify against current Meta documentation when bumping the
/// Graph API version (the URL above is the canonical reference, not
/// this comment).
///
/// All other Cloud API error codes collapse to
/// `"whatsapp_api_error"`; non-JSON or shape-mismatched bodies
/// collapse to `"whatsapp_api_error"` as well. Network failures
/// before a response arrives are classified upstream as
/// `"network_error"`.
pub(crate) fn classify_send_error(response_body: &str) -> &'static str {
    let code = serde_json::from_str::<serde_json::Value>(response_body)
        .ok()
        .as_ref()
        .and_then(|v| v.get("error"))
        .and_then(|e| e.get("code"))
        .and_then(|c| c.as_i64());
    match code {
        Some(131047) | Some(131026) => "window_closed",
        _ => "whatsapp_api_error",
    }
}

async fn heartbeat_loop(writer: Arc<Mutex<IpcFrameWriter>>) {
    let mut seq = 0u64;
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(15));
    loop {
        interval.tick().await;
        seq += 1;
        let mut msg = capnp::message::Builder::new_default();
        convert::build_heartbeat(&mut msg, seq);
        let mut w = writer.lock().await;
        if let Err(e) = w.write_message(&msg).await {
            tracing::error!("Heartbeat send failed: {e}");
            break;
        }
    }
}

fn extract_header(request: &str, name: &str) -> String {
    let lower = name.to_lowercase();
    for line in request.lines() {
        if let Some((key, value)) = line.split_once(':')
            && key.trim().to_lowercase() == lower
        {
            return value.trim().to_string();
        }
    }
    String::new()
}

/// Verify the HMAC-SHA256 signature from Meta's webhook. Uses
/// `Mac::verify_slice` on the decoded bytes so the comparison is
/// constant-time; the prior implementation compared hex strings
/// with `==`, which short-circuited on the first mismatched byte
/// and leaked per-byte validity via response latency. The hex
/// length is checked before decoding so a malformed signature
/// cannot produce a parallel length oracle.
pub(crate) fn verify_signature(app_secret: &str, body: &str, signature_header: &str) -> bool {
    let Some(hex_sig) = signature_header.strip_prefix("sha256=") else {
        return false;
    };
    // HMAC-SHA256 is 32 bytes encoded as 64 lowercase hex characters.
    if hex_sig.len() != 64 {
        return false;
    }
    let Ok(sig_bytes) = decode_hex_32(hex_sig) else {
        return false;
    };
    let Ok(mut mac) = HmacSha256::new_from_slice(app_secret.as_bytes()) else {
        return false;
    };
    mac.update(body.as_bytes());
    mac.verify_slice(&sig_bytes).is_ok()
}

fn decode_hex_32(s: &str) -> Result<[u8; 32], ()> {
    let bytes = s.as_bytes();
    if bytes.len() != 64 {
        return Err(());
    }
    let mut out = [0u8; 32];
    for i in 0..32 {
        let hi = hex_nibble(bytes[i * 2])?;
        let lo = hex_nibble(bytes[i * 2 + 1])?;
        out[i] = (hi << 4) | lo;
    }
    Ok(out)
}

fn hex_nibble(b: u8) -> Result<u8, ()> {
    match b {
        b'0'..=b'9' => Ok(b - b'0'),
        b'a'..=b'f' => Ok(b - b'a' + 10),
        b'A'..=b'F' => Ok(b - b'A' + 10),
        _ => Err(()),
    }
}
