//! Fixed, bounded WebAssembly bridge for Rust's `regex` crate 1.13.1.
//!
//! User-controlled pattern, subject and replacement values enter through
//! deserialized JSON. They are never interpreted as Rust or JavaScript source.

#![forbid(unsafe_code)]

use regex::{Captures, Regex, RegexBuilder};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet};
use wasm_bindgen::prelude::*;

const BRIDGE_ABI_VERSION: u32 = 1;
const REGEX_CRATE_VERSION: &str = "1.13.1";
const RUSTC_VERSION: &str = env!("REGEX_TOOLS_RUSTC_VERSION");
const EXPECTED_RUSTC_VERSION: &str = "rustc 1.97.1 (8bab26f4f 2026-07-14)";
const MAXIMUM_PATTERN_BYTES: usize = 256 * 1024;
const MAXIMUM_SUBJECT_BYTES: usize = 16 * 1024 * 1024;
const MAXIMUM_REPLACEMENT_BYTES: usize = 256 * 1024;
const MAXIMUM_MATCHES: usize = 10_000;
const MAXIMUM_CAPTURE_ROWS: usize = 100_000;
const MAXIMUM_CAPTURE_GROUPS: usize = 1_000;
const MAXIMUM_OUTPUT_BYTES: usize = 64 * 1024 * 1024;
const MAXIMUM_REQUEST_JSON_BYTES: usize =
    6 * (MAXIMUM_SUBJECT_BYTES + MAXIMUM_PATTERN_BYTES + MAXIMUM_REPLACEMENT_BYTES)
        + 64 * 1024;
const MAXIMUM_ERROR_MESSAGE_BYTES: usize = 1_024;
const COMPILED_SIZE_LIMIT_BYTES: usize = 10 * 1024 * 1024;
const DFA_SIZE_LIMIT_BYTES: usize = 2 * 1024 * 1024;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct BridgeRequest {
    abi: u32,
    operation: String,
    pattern: String,
    flags: String,
    options: BTreeMap<String, Value>,
    subject: String,
    scan_all: bool,
    maximum_matches: usize,
    maximum_capture_rows: usize,
    replacement: Option<String>,
    maximum_output_bytes: Option<usize>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct BridgeFailure {
    abi: u32,
    ok: bool,
    phase: &'static str,
    code: &'static str,
    message: String,
}

#[derive(Serialize)]
struct NativeMatch {
    span: [usize; 2],
    captures: Vec<Option<[usize; 2]>>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct BridgeSuccess {
    abi: u32,
    ok: bool,
    group_count: usize,
    group_names: Vec<(usize, String)>,
    matches: Vec<NativeMatch>,
    results_truncated: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    output_base64: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    output_bytes: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    output_truncated: Option<bool>,
}

fn bounded_message(value: impl std::fmt::Display) -> String {
    let value = value.to_string();
    if value.len() <= MAXIMUM_ERROR_MESSAGE_BYTES {
        return value;
    }
    let mut end = MAXIMUM_ERROR_MESSAGE_BYTES - "…".len();
    while !value.is_char_boundary(end) {
        end -= 1;
    }
    format!("{}…", &value[..end])
}

fn serialize<T: Serialize>(value: &T) -> String {
    match serde_json::to_string(value) {
        Ok(encoded) => encoded,
        Err(_) => {
            "{\"abi\":1,\"ok\":false,\"phase\":\"bridge\",\"code\":\"serialization-error\",\"message\":\"The Rust bridge could not serialize its bounded result.\"}".to_owned()
        }
    }
}

fn failure(phase: &'static str, code: &'static str, message: impl std::fmt::Display) -> String {
    serialize(&BridgeFailure {
        abi: BRIDGE_ABI_VERSION,
        ok: false,
        phase,
        code,
        message: bounded_message(message),
    })
}

fn validate_request(request: &BridgeRequest) -> Result<(), String> {
    if request.abi != BRIDGE_ABI_VERSION {
        return Err(failure(
            "bridge",
            "abi-mismatch",
            "The bridge ABI version is unsupported.",
        ));
    }
    if request.operation != "execute" && request.operation != "replace" {
        return Err(failure(
            "bridge",
            "invalid-operation",
            "The bridge operation is unsupported.",
        ));
    }
    if !request.options.is_empty() {
        return Err(failure(
            "bridge",
            "unsupported-option",
            "Rust regex does not accept engine options.",
        ));
    }
    if request.pattern.len() > MAXIMUM_PATTERN_BYTES {
        return Err(failure(
            "bridge",
            "pattern-limit",
            "The pattern exceeds the bridge limit.",
        ));
    }
    if request.subject.len() > MAXIMUM_SUBJECT_BYTES {
        return Err(failure(
            "bridge",
            "subject-limit",
            "The subject exceeds the bridge limit.",
        ));
    }
    if request.maximum_matches == 0
        || request.maximum_matches > MAXIMUM_MATCHES
        || request.maximum_capture_rows == 0
        || request.maximum_capture_rows > MAXIMUM_CAPTURE_ROWS
    {
        return Err(failure(
            "bridge",
            "result-limit",
            "The requested result bounds are outside the bridge contract.",
        ));
    }
    let mut seen = BTreeSet::new();
    for flag in request.flags.chars() {
        if !"gimsUuxR".contains(flag) || !seen.insert(flag) {
            return Err(failure(
                "bridge",
                "unsupported-flag",
                "The request contains an unsupported or duplicate Rust flag.",
            ));
        }
    }
    if request.operation == "execute" {
        if request.replacement.is_some() || request.maximum_output_bytes.is_some() {
            return Err(failure(
                "bridge",
                "invalid-request",
                "Execution requests cannot contain replacement fields.",
            ));
        }
        return Ok(());
    }
    let Some(replacement) = &request.replacement else {
        return Err(failure(
            "bridge",
            "invalid-request",
            "Replacement requests require a template.",
        ));
    };
    let Some(output_limit) = request.maximum_output_bytes else {
        return Err(failure(
            "bridge",
            "invalid-request",
            "Replacement requests require an output bound.",
        ));
    };
    if replacement.len() > MAXIMUM_REPLACEMENT_BYTES {
        return Err(failure(
            "bridge",
            "replacement-limit",
            "The replacement exceeds the bridge limit.",
        ));
    }
    if output_limit == 0 || output_limit > MAXIMUM_OUTPUT_BYTES {
        return Err(failure(
            "bridge",
            "output-limit",
            "The output bound is outside the bridge contract.",
        ));
    }
    Ok(())
}

fn compile(request: &BridgeRequest) -> Result<Regex, String> {
    let has = |flag| request.flags.contains(flag);
    let mut builder = RegexBuilder::new(&request.pattern);
    builder
        .case_insensitive(has('i'))
        .multi_line(has('m'))
        .dot_matches_new_line(has('s'))
        .swap_greed(has('U'))
        // Unicode is Rust regex's native default. The accepted `u` flag is an
        // explicit affirmation, not a request to disable Unicode when absent.
        .unicode(true)
        .ignore_whitespace(has('x'))
        .crlf(has('R'))
        .size_limit(COMPILED_SIZE_LIMIT_BYTES)
        .dfa_size_limit(DFA_SIZE_LIMIT_BYTES);
    builder
        .build()
        .map_err(|error| failure("compile", "compile-error", error))
}

fn materialize(captures: &Captures<'_>, group_count: usize) -> NativeMatch {
    let whole = captures
        .get(0)
        .expect("regex captures always contain the complete match");
    let groups = (1..=group_count)
        .map(|group| {
            captures
                .get(group)
                .map(|capture| [capture.start(), capture.end()])
        })
        .collect();
    NativeMatch {
        span: [whole.start(), whole.end()],
        captures: groups,
    }
}

fn collect_matches(
    compiled: &Regex,
    request: &BridgeRequest,
    group_count: usize,
) -> (Vec<NativeMatch>, bool) {
    let rows_per_match = group_count.max(1);
    let allowed = request
        .maximum_matches
        .min(request.maximum_capture_rows / rows_per_match);
    let global = request.flags.contains('g');
    let mut records = Vec::with_capacity(allowed.min(256));
    let mut truncated = false;
    if global {
        for captures in compiled.captures_iter(&request.subject) {
            if records.len() == allowed {
                truncated = true;
                break;
            }
            records.push(materialize(&captures, group_count));
        }
    } else if let Some(captures) = compiled.captures(&request.subject) {
        if allowed == 0 {
            truncated = true;
        } else {
            records.push(materialize(&captures, group_count));
        }
    }
    (records, truncated)
}

struct BoundedOutput {
    value: String,
    maximum: usize,
    truncated: bool,
}

impl BoundedOutput {
    fn new(maximum: usize) -> Self {
        Self {
            value: String::with_capacity(maximum.min(64 * 1024)),
            maximum,
            truncated: false,
        }
    }

    fn append(&mut self, value: &str) {
        if self.truncated || value.is_empty() {
            return;
        }
        let remaining = self.maximum - self.value.len();
        if value.len() <= remaining {
            self.value.push_str(value);
            return;
        }
        let mut end = remaining;
        while !value.is_char_boundary(end) {
            end -= 1;
        }
        self.value.push_str(&value[..end]);
        self.truncated = true;
    }
}

enum ReplacementReference<'a> {
    Number(usize),
    Named(&'a str),
}

fn capture_reference(replacement: &str) -> Option<(ReplacementReference<'_>, usize)> {
    let bytes = replacement.as_bytes();
    if bytes.len() <= 1 || bytes[0] != b'$' {
        return None;
    }
    let (name, end) = if bytes[1] == b'{' {
        let close = bytes[2..].iter().position(|&byte| byte == b'}')? + 2;
        (&replacement[2..close], close + 1)
    } else {
        let mut end = 1;
        while bytes
            .get(end)
            .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
        {
            end += 1;
        }
        if end == 1 {
            return None;
        }
        (&replacement[1..end], end)
    };
    let reference = match name.parse::<usize>() {
        Ok(number) => ReplacementReference::Number(number),
        Err(_) => ReplacementReference::Named(name),
    };
    Some((reference, end))
}

fn expand_replacement_bounded(
    output: &mut BoundedOutput,
    captures: &Captures<'_>,
    mut replacement: &str,
) {
    while !replacement.is_empty() && !output.truncated {
        let Some(dollar) = replacement.as_bytes().iter().position(|&byte| byte == b'$') else {
            output.append(replacement);
            return;
        };
        output.append(&replacement[..dollar]);
        if output.truncated {
            return;
        }
        replacement = &replacement[dollar..];
        if replacement.as_bytes().get(1) == Some(&b'$') {
            output.append("$");
            replacement = &replacement[2..];
            continue;
        }
        let Some((reference, end)) = capture_reference(replacement) else {
            output.append("$");
            replacement = &replacement[1..];
            continue;
        };
        replacement = &replacement[end..];
        let capture = match reference {
            ReplacementReference::Number(number) => captures.get(number),
            ReplacementReference::Named(name) => captures.name(name),
        };
        if let Some(capture) = capture {
            output.append(capture.as_str());
        }
    }
}

fn encode_base64(input: &[u8]) -> String {
    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut encoded = String::with_capacity(input.len().div_ceil(3) * 4);
    for chunk in input.chunks(3) {
        let first = chunk[0];
        let second = chunk.get(1).copied().unwrap_or(0);
        let third = chunk.get(2).copied().unwrap_or(0);
        encoded.push(ALPHABET[usize::from(first >> 2)] as char);
        encoded.push(ALPHABET[usize::from(((first & 0x03) << 4) | (second >> 4))] as char);
        encoded.push(if chunk.len() > 1 {
            ALPHABET[usize::from(((second & 0x0f) << 2) | (third >> 6))] as char
        } else {
            '='
        });
        encoded.push(if chunk.len() > 2 {
            ALPHABET[usize::from(third & 0x3f)] as char
        } else {
            '='
        });
    }
    encoded
}

fn replace_bounded(
    compiled: &Regex,
    request: &BridgeRequest,
    accepted_matches: usize,
) -> (String, usize, bool) {
    let replacement = request
        .replacement
        .as_deref()
        .expect("validated replacement request");
    let output_limit = request
        .maximum_output_bytes
        .expect("validated replacement request");
    let mut output = BoundedOutput::new(output_limit);
    let mut cursor = 0;
    let global = request.flags.contains('g');
    let maximum = if global {
        accepted_matches
    } else {
        accepted_matches.min(1)
    };
    for captures in compiled.captures_iter(&request.subject).take(maximum) {
        let whole = captures
            .get(0)
            .expect("regex captures always contain the complete match");
        output.append(&request.subject[cursor..whole.start()]);
        expand_replacement_bounded(&mut output, &captures, replacement);
        cursor = whole.end();
    }
    output.append(&request.subject[cursor..]);
    let bytes = output.value.len();
    (
        encode_base64(output.value.as_bytes()),
        bytes,
        output.truncated,
    )
}

fn execute(encoded: &str) -> String {
    if encoded.len() > MAXIMUM_REQUEST_JSON_BYTES {
        return failure(
            "bridge",
            "request-limit",
            "The encoded request exceeds the bridge limit.",
        );
    }
    let request: BridgeRequest = match serde_json::from_str(encoded) {
        Ok(request) => request,
        Err(error) => return failure("bridge", "invalid-request", error),
    };
    if let Err(error) = validate_request(&request) {
        return error;
    }
    // Reading the field also keeps the transport contract explicit: matching
    // is selected by the normalized native `g` flag, while scanAll records the
    // application request that caused an internal `g` to be added.
    let _scan_all = request.scan_all;
    let compiled = match compile(&request) {
        Ok(compiled) => compiled,
        Err(error) => return error,
    };
    let group_count = compiled.captures_len() - 1;
    if group_count > MAXIMUM_CAPTURE_GROUPS {
        return failure(
            "compile",
            "capture-group-limit",
            format!(
                "The pattern declares {group_count} capture groups; the bridge limit is {MAXIMUM_CAPTURE_GROUPS}."
            ),
        );
    }
    let group_names = compiled
        .capture_names()
        .enumerate()
        .filter_map(|(number, name)| name.map(|name| (number, name.to_owned())))
        .filter(|(number, _)| *number > 0)
        .collect();
    let (matches, results_truncated) = collect_matches(&compiled, &request, group_count);
    let mut result = BridgeSuccess {
        abi: BRIDGE_ABI_VERSION,
        ok: true,
        group_count,
        group_names,
        matches,
        results_truncated,
        output_base64: None,
        output_bytes: None,
        output_truncated: None,
    };
    if request.operation == "replace" {
        let (output_base64, output_bytes, output_truncated) =
            replace_bounded(&compiled, &request, result.matches.len());
        result.output_base64 = Some(output_base64);
        result.output_bytes = Some(output_bytes);
        result.output_truncated = Some(output_truncated);
    }
    serialize(&result)
}

fn self_test() -> bool {
    if RUSTC_VERSION != EXPECTED_RUSTC_VERSION || encode_base64("😀".as_bytes()) != "8J+YgA==" {
        return false;
    }
    let Ok(compiled) = Regex::new(r"(?P<word>😀+)") else {
        return false;
    };
    let Some(captures) = compiled.captures("x😀😀") else {
        return false;
    };
    let Some(whole) = captures.get(0) else {
        return false;
    };
    let Some(word) = captures.name("word") else {
        return false;
    };
    if whole.start() != 1 || whole.end() != 9 || word.start() != 1 || word.end() != 9 {
        return false;
    }
    for template in [
        "${word}-$word-$$",
        "$1x|${1}x",
        "${missing}|$|${word",
        "$01|${01}|$123456789012345678901234567890",
        "${}|${word}",
    ] {
        let mut native = String::new();
        captures.expand(template, &mut native);
        let mut bounded = BoundedOutput::new(4 * 1024);
        expand_replacement_bounded(&mut bounded, &captures, template);
        if bounded.truncated || bounded.value != native {
            return false;
        }
    }
    let mut adversarial = BoundedOutput::new(5);
    expand_replacement_bounded(&mut adversarial, &captures, &"${word}".repeat(32 * 1024));
    adversarial.truncated && adversarial.value == "😀"
}

#[wasm_bindgen]
pub fn identity() -> String {
    let mut identity = Map::new();
    identity.insert(
        "bridge".to_owned(),
        Value::String("regex-tools-rust-json".to_owned()),
    );
    identity.insert("bridgeVersion".to_owned(), Value::from(BRIDGE_ABI_VERSION));
    identity.insert(
        "engine".to_owned(),
        Value::String("Rust regex crate".to_owned()),
    );
    identity.insert(
        "engineVersion".to_owned(),
        Value::String(REGEX_CRATE_VERSION.to_owned()),
    );
    identity.insert(
        "offsetUnit".to_owned(),
        Value::String("utf8-byte".to_owned()),
    );
    identity.insert(
        "rustcVersion".to_owned(),
        Value::String(RUSTC_VERSION.to_owned()),
    );
    identity.insert("selfTest".to_owned(), Value::Bool(self_test()));
    let mut envelope = Map::new();
    envelope.insert("abi".to_owned(), Value::from(BRIDGE_ABI_VERSION));
    envelope.insert("ok".to_owned(), Value::Bool(true));
    envelope.insert("identity".to_owned(), Value::Object(identity));
    serialize(&Value::Object(envelope))
}

#[wasm_bindgen]
pub fn run(encoded: &str) -> String {
    execute(encoded)
}
