pub(crate) use _json::module_def;
mod machinery;

#[pymodule]
mod _json {
    use super::machinery;
    use crate::vm::{
        AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
        builtins::{PyBaseExceptionRef, PyStrRef, PyType},
        convert::ToPyResult,
        function::{IntoFuncArgs, OptionalArg},
        protocol::PyIterReturn,
        types::{Callable, Constructor},
    };
    use core::str::FromStr;
    use malachite_bigint::BigInt;
    use rustpython_common::wtf8::Wtf8Buf;
    use std::collections::HashMap;

    /// Skip JSON whitespace characters (space, tab, newline, carriage return).
    /// Works with a byte slice and returns the number of bytes skipped.
    /// Since all JSON whitespace chars are ASCII, bytes == chars.
    #[inline]
    fn skip_whitespace(bytes: &[u8]) -> usize {
        flame_guard!("_json::skip_whitespace");
        let mut count = 0;
        for &b in bytes {
            match b {
                b' ' | b'\t' | b'\n' | b'\r' => count += 1,
                _ => break,
            }
        }
        count
    }

    /// Check if a byte slice starts with a given ASCII pattern.
    #[inline]
    fn starts_with_bytes(bytes: &[u8], pattern: &[u8]) -> bool {
        bytes.len() >= pattern.len() && &bytes[..pattern.len()] == pattern
    }

    #[pyattr(name = "make_scanner")]
    #[pyclass(name = "Scanner", traverse)]
    #[derive(Debug, PyPayload)]
    struct JsonScanner {
        #[pytraverse(skip)]
        strict: bool,
        object_hook: Option<PyObjectRef>,
        object_pairs_hook: Option<PyObjectRef>,
        parse_float: Option<PyObjectRef>,
        parse_int: Option<PyObjectRef>,
        parse_constant: PyObjectRef,
        ctx: PyObjectRef,
    }

    impl Constructor for JsonScanner {
        type Args = PyObjectRef;

        fn py_new(_cls: &Py<PyType>, ctx: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
            let strict = ctx.get_attr("strict", vm)?.try_to_bool(vm)?;
            let object_hook = vm.option_if_none(ctx.get_attr("object_hook", vm)?);
            let object_pairs_hook = vm.option_if_none(ctx.get_attr("object_pairs_hook", vm)?);
            let parse_float = ctx.get_attr("parse_float", vm)?;
            let parse_float = if vm.is_none(&parse_float) || parse_float.is(vm.ctx.types.float_type)
            {
                None
            } else {
                Some(parse_float)
            };
            let parse_int = ctx.get_attr("parse_int", vm)?;
            let parse_int = if vm.is_none(&parse_int) || parse_int.is(vm.ctx.types.int_type) {
                None
            } else {
                Some(parse_int)
            };
            let parse_constant = ctx.get_attr("parse_constant", vm)?;

            Ok(Self {
                strict,
                object_hook,
                object_pairs_hook,
                parse_float,
                parse_int,
                parse_constant,
                ctx,
            })
        }
    }

    #[pyclass(with(Callable, Constructor))]
    impl JsonScanner {
        fn parse(
            &self,
            pystr: PyStrRef,
            char_idx: usize,
            byte_idx: usize,
            scan_once: PyObjectRef,
            vm: &VirtualMachine,
        ) -> PyResult<PyIterReturn> {
            flame_guard!("JsonScanner::parse");
            let bytes = pystr.as_bytes();
            let wtf8 = pystr.as_wtf8();

            let first_byte = match bytes.get(byte_idx) {
                Some(&b) => b,
                None => {
                    return Ok(PyIterReturn::StopIteration(Some(
                        vm.ctx.new_int(char_idx).into(),
                    )));
                }
            };

            match first_byte {
                b'"' => {
                    // Parse string - pass slice starting after the quote
                    let (wtf8_result, chars_consumed, _bytes_consumed) =
                        machinery::scanstring(&wtf8[byte_idx + 1..], char_idx + 1, self.strict)
                            .map_err(|e| py_decode_error(e, pystr.clone(), vm))?;
                    let end_char_idx = char_idx + 1 + chars_consumed;
                    return Ok(PyIterReturn::Return(
                        vm.new_tuple((wtf8_result, end_char_idx)).into(),
                    ));
                }
                b'{' => {
                    // Parse object in Rust
                    let mut memo = HashMap::new();
                    return self
                        .parse_object(pystr, char_idx + 1, byte_idx + 1, &scan_once, &mut memo, vm)
                        .map(|(obj, end_char, _end_byte)| {
                            PyIterReturn::Return(vm.new_tuple((obj, end_char)).into())
                        });
                }
                b'[' => {
                    // Parse array in Rust
                    let mut memo = HashMap::new();
                    return self
                        .parse_array(pystr, char_idx + 1, byte_idx + 1, &scan_once, &mut memo, vm)
                        .map(|(obj, end_char, _end_byte)| {
                            PyIterReturn::Return(vm.new_tuple((obj, end_char)).into())
                        });
                }
                _ => {}
            }

            let rest = &bytes[byte_idx..];

            macro_rules! parse_const {
                ($s:literal, $val:expr) => {
                    if rest.starts_with($s.as_bytes()) {
                        return Ok(PyIterReturn::Return(
                            vm.new_tuple(($val, char_idx + $s.len())).into(),
                        ));
                    }
                };
            }

            parse_const!("null", vm.ctx.none());
            parse_const!("true", true);
            parse_const!("false", false);

            if let Some((res, len)) = self.parse_number(rest, vm) {
                return Ok(PyIterReturn::Return(
                    vm.new_tuple((res?, char_idx + len)).into(),
                ));
            }

            macro_rules! parse_constant {
                ($s:literal) => {
                    if rest.starts_with($s.as_bytes()) {
                        return Ok(PyIterReturn::Return(
                            vm.new_tuple((
                                self.parse_constant.call(($s,), vm)?,
                                char_idx + $s.len(),
                            ))
                            .into(),
                        ));
                    }
                };
            }

            parse_constant!("NaN");
            parse_constant!("Infinity");
            parse_constant!("-Infinity");

            Ok(PyIterReturn::StopIteration(Some(
                vm.ctx.new_int(char_idx).into(),
            )))
        }

        fn parse_number(&self, bytes: &[u8], vm: &VirtualMachine) -> Option<(PyResult, usize)> {
            flame_guard!("JsonScanner::parse_number");
            // RFC 8259 defines JSON numbers in ASCII syntax, including digits,
            // '-', '.', 'e'/'E', and an optional exponent sign, so byte iteration
            // is equivalent to char iteration here.
            let mut i = 0;
            if bytes.get(i) == Some(&b'-') {
                i += 1;
            }
            match bytes.get(i) {
                Some(b'0') => i += 1,
                Some(b'1'..=b'9') => {
                    i += 1;
                    while matches!(bytes.get(i), Some(b'0'..=b'9')) {
                        i += 1;
                    }
                }
                _ => return None,
            }

            let mut is_float = false;
            if bytes.get(i) == Some(&b'.') && matches!(bytes.get(i + 1), Some(b'0'..=b'9')) {
                is_float = true;
                i += 2;
                while matches!(bytes.get(i), Some(b'0'..=b'9')) {
                    i += 1;
                }
            }

            if matches!(bytes.get(i), Some(b'e' | b'E')) {
                let mut exponent_end = i + 1;
                if matches!(bytes.get(exponent_end), Some(b'+' | b'-')) {
                    exponent_end += 1;
                }
                if matches!(bytes.get(exponent_end), Some(b'0'..=b'9')) {
                    is_float = true;
                    exponent_end += 1;
                    while matches!(bytes.get(exponent_end), Some(b'0'..=b'9')) {
                        exponent_end += 1;
                    }
                    i = exponent_end;
                }
            }

            // SAFETY: the loop above accepts only ASCII bytes, so bytes[..i] is valid UTF-8.
            let buf = unsafe { core::str::from_utf8_unchecked(&bytes[..i]) };
            let ret = if is_float {
                // float
                if let Some(ref parse_float) = self.parse_float {
                    parse_float.call((buf,), vm)
                } else {
                    Ok(vm.ctx.new_float(f64::from_str(buf).unwrap()).into())
                }
            } else if let Some(ref parse_int) = self.parse_int {
                parse_int.call((buf,), vm)
            } else {
                Ok(vm.new_pyobj(BigInt::from_str(buf).unwrap()))
            };
            Some((ret, buf.len()))
        }

        /// Parse a JSON object starting after the opening '{'.
        /// Returns (parsed_object, end_char_index, end_byte_index).
        fn parse_object(
            &self,
            pystr: PyStrRef,
            start_char_idx: usize,
            start_byte_idx: usize,
            scan_once: &PyObjectRef,
            memo: &mut HashMap<Wtf8Buf, PyStrRef>,
            vm: &VirtualMachine,
        ) -> PyResult<(PyObjectRef, usize, usize)> {
            flame_guard!("JsonScanner::parse_object");

            let bytes = pystr.as_bytes();
            let wtf8 = pystr.as_wtf8();
            let mut char_idx = start_char_idx;
            let mut byte_idx = start_byte_idx;

            // Skip initial whitespace
            let ws = skip_whitespace(&bytes[byte_idx..]);
            char_idx += ws;
            byte_idx += ws;

            // Check for empty object
            match bytes.get(byte_idx) {
                Some(b'}') => {
                    return self.finalize_object(vec![], char_idx + 1, byte_idx + 1, vm);
                }
                Some(b'"') => {
                    // Continue to parse first key
                }
                _ => {
                    return Err(self.make_decode_error(
                        "Expecting property name enclosed in double quotes",
                        pystr,
                        char_idx,
                        vm,
                    ));
                }
            }

            let mut pairs: Vec<(PyObjectRef, PyObjectRef)> = Vec::new();

            loop {
                // We're now at '"', skip it
                char_idx += 1;
                byte_idx += 1;

                // Parse key string using scanstring with byte slice
                let (key_wtf8, chars_consumed, bytes_consumed) =
                    machinery::scanstring(&wtf8[byte_idx..], char_idx, self.strict)
                        .map_err(|e| py_decode_error(e, pystr.clone(), vm))?;

                char_idx += chars_consumed;
                byte_idx += bytes_consumed;

                // Key memoization - reuse existing key strings.
                // Keyed by Wtf8Buf so lone surrogates in keys (legal per Python str)
                // are preserved; using String here would lossy-collapse surrogates to U+FFFD.
                let key: PyObjectRef = match memo.get(&key_wtf8) {
                    Some(cached) => cached.clone().into(),
                    None => {
                        let py_key = vm.ctx.new_str(key_wtf8.clone());
                        memo.insert(key_wtf8, py_key.clone());
                        py_key.into()
                    }
                };

                // Skip whitespace after key
                let ws = skip_whitespace(&bytes[byte_idx..]);
                char_idx += ws;
                byte_idx += ws;

                // Expect ':' delimiter
                match bytes.get(byte_idx) {
                    Some(b':') => {
                        char_idx += 1;
                        byte_idx += 1;
                    }
                    _ => {
                        return Err(self.make_decode_error(
                            "Expecting ':' delimiter",
                            pystr,
                            char_idx,
                            vm,
                        ));
                    }
                }

                // Skip whitespace after ':'
                let ws = skip_whitespace(&bytes[byte_idx..]);
                char_idx += ws;
                byte_idx += ws;

                // Parse value recursively
                let (value, value_char_end, value_byte_end) =
                    self.call_scan_once(scan_once, pystr.clone(), char_idx, byte_idx, memo, vm)?;

                pairs.push((key, value));
                char_idx = value_char_end;
                byte_idx = value_byte_end;

                // Skip whitespace after value
                let ws = skip_whitespace(&bytes[byte_idx..]);
                char_idx += ws;
                byte_idx += ws;

                // Check for ',' or '}'
                match bytes.get(byte_idx) {
                    Some(b'}') => {
                        char_idx += 1;
                        byte_idx += 1;
                        break;
                    }
                    Some(b',') => {
                        let comma_char_idx = char_idx;
                        char_idx += 1;
                        byte_idx += 1;

                        // Skip whitespace after comma
                        let ws = skip_whitespace(&bytes[byte_idx..]);
                        char_idx += ws;
                        byte_idx += ws;

                        // Next must be '"'
                        match bytes.get(byte_idx) {
                            Some(b'"') => {
                                // Continue to next key-value pair
                            }
                            Some(b'}') => {
                                // Trailing comma before end of object
                                return Err(self.make_decode_error(
                                    "Illegal trailing comma before end of object",
                                    pystr,
                                    comma_char_idx,
                                    vm,
                                ));
                            }
                            _ => {
                                return Err(self.make_decode_error(
                                    "Expecting property name enclosed in double quotes",
                                    pystr,
                                    char_idx,
                                    vm,
                                ));
                            }
                        }
                    }
                    _ => {
                        return Err(self.make_decode_error(
                            "Expecting ',' delimiter",
                            pystr,
                            char_idx,
                            vm,
                        ));
                    }
                }
            }

            self.finalize_object(pairs, char_idx, byte_idx, vm)
        }

        /// Parse a JSON array starting after the opening '['.
        /// Returns (parsed_array, end_char_index, end_byte_index).
        fn parse_array(
            &self,
            pystr: PyStrRef,
            start_char_idx: usize,
            start_byte_idx: usize,
            scan_once: &PyObjectRef,
            memo: &mut HashMap<Wtf8Buf, PyStrRef>,
            vm: &VirtualMachine,
        ) -> PyResult<(PyObjectRef, usize, usize)> {
            flame_guard!("JsonScanner::parse_array");

            let bytes = pystr.as_bytes();
            let mut char_idx = start_char_idx;
            let mut byte_idx = start_byte_idx;

            // Skip initial whitespace
            let ws = skip_whitespace(&bytes[byte_idx..]);
            char_idx += ws;
            byte_idx += ws;

            // Check for empty array
            if bytes.get(byte_idx) == Some(&b']') {
                return Ok((vm.ctx.new_list(vec![]).into(), char_idx + 1, byte_idx + 1));
            }

            let mut values: Vec<PyObjectRef> = Vec::new();

            loop {
                // Parse value
                let (value, value_char_end, value_byte_end) =
                    self.call_scan_once(scan_once, pystr.clone(), char_idx, byte_idx, memo, vm)?;

                values.push(value);
                char_idx = value_char_end;
                byte_idx = value_byte_end;

                // Skip whitespace after value
                let ws = skip_whitespace(&bytes[byte_idx..]);
                char_idx += ws;
                byte_idx += ws;

                match bytes.get(byte_idx) {
                    Some(b']') => {
                        char_idx += 1;
                        byte_idx += 1;
                        break;
                    }
                    Some(b',') => {
                        let comma_char_idx = char_idx;
                        char_idx += 1;
                        byte_idx += 1;

                        // Skip whitespace after comma
                        let ws = skip_whitespace(&bytes[byte_idx..]);
                        char_idx += ws;
                        byte_idx += ws;

                        // Check for trailing comma
                        if bytes.get(byte_idx) == Some(&b']') {
                            return Err(self.make_decode_error(
                                "Illegal trailing comma before end of array",
                                pystr,
                                comma_char_idx,
                                vm,
                            ));
                        }
                    }
                    _ => {
                        return Err(self.make_decode_error(
                            "Expecting ',' delimiter",
                            pystr,
                            char_idx,
                            vm,
                        ));
                    }
                }
            }

            Ok((vm.ctx.new_list(values).into(), char_idx, byte_idx))
        }

        /// Finalize object construction with hooks.
        fn finalize_object(
            &self,
            pairs: Vec<(PyObjectRef, PyObjectRef)>,
            end_char_idx: usize,
            end_byte_idx: usize,
            vm: &VirtualMachine,
        ) -> PyResult<(PyObjectRef, usize, usize)> {
            let result = if let Some(ref pairs_hook) = self.object_pairs_hook {
                // object_pairs_hook takes priority - pass list of tuples
                let pairs_list: Vec<PyObjectRef> = pairs
                    .into_iter()
                    .map(|(k, v)| vm.new_tuple((k, v)).into())
                    .collect();
                pairs_hook.call((vm.ctx.new_list(pairs_list),), vm)?
            } else {
                // Build a dict from pairs
                let dict = vm.ctx.new_dict();
                for (key, value) in pairs {
                    dict.set_item(&*key, value, vm)?;
                }

                // Apply object_hook if present
                let dict_obj: PyObjectRef = dict.into();
                if let Some(ref hook) = self.object_hook {
                    hook.call((dict_obj,), vm)?
                } else {
                    dict_obj
                }
            };

            Ok((result, end_char_idx, end_byte_idx))
        }

        /// Call scan_once and handle the result.
        /// Returns (value, end_char_idx, end_byte_idx).
        fn call_scan_once(
            &self,
            scan_once: &PyObjectRef,
            pystr: PyStrRef,
            char_idx: usize,
            byte_idx: usize,
            memo: &mut HashMap<Wtf8Buf, PyStrRef>,
            vm: &VirtualMachine,
        ) -> PyResult<(PyObjectRef, usize, usize)> {
            // Recursion guard: parse_object/parse_array recurse into call_scan_once
            // for each child value. Without this, a deeply-nested input like
            // `'[' * 50000 + ']' * 50000` overflows the native Rust stack and
            // crashes the process with SIGSEGV. Matches CPython's
            // _Py_EnterRecursiveCall in Modules/_json.c.
            vm.with_recursion("while decoding a JSON object from a string", || {
                let bytes = pystr.as_bytes();
                let wtf8 = pystr.as_wtf8();

                let first_byte = match bytes.get(byte_idx) {
                    Some(&b) => b,
                    None => {
                        return Err(self.make_decode_error("Expecting value", pystr, char_idx, vm));
                    }
                };

                match first_byte {
                    b'"' => {
                        // String - pass slice starting after the quote.
                        // Feed the Wtf8Buf directly to new_str; routing through
                        // .to_string() here would lossy-collapse surrogates to U+FFFD.
                        let (wtf8_result, chars_consumed, bytes_consumed) =
                            machinery::scanstring(&wtf8[byte_idx + 1..], char_idx + 1, self.strict)
                                .map_err(|e| py_decode_error(e, pystr.clone(), vm))?;
                        let py_str = vm.ctx.new_str(wtf8_result);
                        Ok((
                            py_str.into(),
                            char_idx + 1 + chars_consumed,
                            byte_idx + 1 + bytes_consumed,
                        ))
                    }
                    b'{' => {
                        // Object
                        self.parse_object(pystr, char_idx + 1, byte_idx + 1, scan_once, memo, vm)
                    }
                    b'[' => {
                        // Array
                        self.parse_array(pystr, char_idx + 1, byte_idx + 1, scan_once, memo, vm)
                    }
                    b'n' if starts_with_bytes(&bytes[byte_idx..], b"null") => {
                        // null
                        Ok((vm.ctx.none(), char_idx + 4, byte_idx + 4))
                    }
                    b't' if starts_with_bytes(&bytes[byte_idx..], b"true") => {
                        // true
                        Ok((vm.ctx.new_bool(true).into(), char_idx + 4, byte_idx + 4))
                    }
                    b'f' if starts_with_bytes(&bytes[byte_idx..], b"false") => {
                        // false
                        Ok((vm.ctx.new_bool(false).into(), char_idx + 5, byte_idx + 5))
                    }
                    b'N' if starts_with_bytes(&bytes[byte_idx..], b"NaN") => {
                        // NaN
                        let result = self.parse_constant.call(("NaN",), vm)?;
                        Ok((result, char_idx + 3, byte_idx + 3))
                    }
                    b'I' if starts_with_bytes(&bytes[byte_idx..], b"Infinity") => {
                        // Infinity
                        let result = self.parse_constant.call(("Infinity",), vm)?;
                        Ok((result, char_idx + 8, byte_idx + 8))
                    }
                    b'-' => {
                        // -Infinity or negative number
                        if starts_with_bytes(&bytes[byte_idx..], b"-Infinity") {
                            let result = self.parse_constant.call(("-Infinity",), vm)?;
                            return Ok((result, char_idx + 9, byte_idx + 9));
                        }
                        // Negative number - numbers are ASCII so len == bytes
                        if let Some((result, len)) = self.parse_number(&bytes[byte_idx..], vm) {
                            return Ok((result?, char_idx + len, byte_idx + len));
                        }
                        Err(self.make_decode_error("Expecting value", pystr, char_idx, vm))
                    }
                    b'0'..=b'9' => {
                        // Positive number - numbers are ASCII so len == bytes
                        if let Some((result, len)) = self.parse_number(&bytes[byte_idx..], vm) {
                            return Ok((result?, char_idx + len, byte_idx + len));
                        }
                        Err(self.make_decode_error("Expecting value", pystr, char_idx, vm))
                    }
                    _ => {
                        // Fall back to scan_once for unrecognized input
                        // Note: This path requires char_idx for Python compatibility
                        let result = scan_once.call((pystr.clone(), char_idx as isize), vm);

                        match result {
                            Ok(tuple) => {
                                use crate::vm::builtins::PyTupleRef;
                                let tuple: PyTupleRef = tuple.try_into_value(vm)?;
                                if tuple.len() != 2 {
                                    return Err(vm.new_value_error("scan_once must return 2-tuple"));
                                }
                                let value = tuple.as_slice()[0].clone();
                                let end_char_idx: isize = tuple.as_slice()[1].try_to_value(vm)?;
                                // For fallback, we need to calculate byte_idx from char_idx
                                // This is expensive but fallback should be rare
                                let end_byte_idx = wtf8
                                    .code_point_indices()
                                    .nth(end_char_idx as usize)
                                    .map_or(wtf8.len(), |(i, _)| i);
                                Ok((value, end_char_idx as usize, end_byte_idx))
                            }
                            Err(err) if err.fast_isinstance(vm.ctx.exceptions.stop_iteration) => {
                                Err(self.make_decode_error("Expecting value", pystr, char_idx, vm))
                            }
                            Err(err) => Err(err),
                        }
                    }
                }
            })
        }

        /// Create a decode error.
        fn make_decode_error(
            &self,
            msg: &str,
            s: PyStrRef,
            pos: usize,
            vm: &VirtualMachine,
        ) -> PyBaseExceptionRef {
            let err = machinery::DecodeError::new(msg, pos);
            py_decode_error(err, s, vm)
        }
    }

    impl Callable for JsonScanner {
        type Args = (PyStrRef, isize);
        fn call(zelf: &Py<Self>, (pystr, char_idx): Self::Args, vm: &VirtualMachine) -> PyResult {
            if char_idx < 0 {
                return Err(vm.new_value_error("idx cannot be negative"));
            }
            let char_idx = char_idx as usize;
            let wtf8 = pystr.as_wtf8();

            // Calculate byte index from char index (O(char_idx) but only at entry point)
            let byte_idx = if char_idx == 0 {
                0
            } else {
                match wtf8.code_point_indices().nth(char_idx) {
                    Some((byte_i, _)) => byte_i,
                    None => {
                        // char_idx is beyond the string length
                        return PyIterReturn::StopIteration(Some(vm.ctx.new_int(char_idx).into()))
                            .to_pyresult(vm);
                    }
                }
            };

            zelf.parse(pystr, char_idx, byte_idx, zelf.to_owned().into(), vm)
                .and_then(|x| x.to_pyresult(vm))
        }
    }

    fn encode_string(wtf8: &rustpython_common::wtf8::Wtf8, ascii_only: bool) -> Wtf8Buf {
        flame_guard!("_json::encode_string");
        let mut buf = Vec::<u8>::with_capacity(wtf8.len() + 2);
        machinery::write_json_string(wtf8, ascii_only, &mut buf)
            // SAFETY: writing to a vec can't fail
            .unwrap_or_else(|_| unsafe { core::hint::unreachable_unchecked() });
        // write_json_string is designed to produce valid WTF-8 bytes:
        // - ASCII control characters and JSON-specials are written as ASCII escapes
        // - Valid Unicode scalars are written as UTF-8 (a subset of WTF-8)
        // - Lone surrogates (ascii_only=false branch only) pass through as the
        //   input's WTF-8 byte sequences unchanged
        // Use the checked constructor so any violation of that invariant
        // surfaces as a panic during testing instead of undefined behavior.
        Wtf8Buf::from_bytes(buf).expect("write_json_string produced invalid WTF-8")
    }

    #[pyfunction]
    fn encode_basestring(s: PyStrRef) -> Wtf8Buf {
        encode_string(s.as_wtf8(), false)
    }

    #[pyfunction]
    fn encode_basestring_ascii(s: PyStrRef) -> Wtf8Buf {
        encode_string(s.as_wtf8(), true)
    }

    fn py_decode_error(
        e: machinery::DecodeError,
        s: PyStrRef,
        vm: &VirtualMachine,
    ) -> PyBaseExceptionRef {
        let get_error = || -> PyResult<_> {
            let cls = vm.try_class("json", "JSONDecodeError")?;
            let exc = PyType::call(&cls, (e.msg, s, e.pos).into_args(vm), vm)?;
            exc.try_into_value(vm)
        };
        match get_error() {
            Ok(x) | Err(x) => x,
        }
    }

    #[pyfunction]
    fn scanstring(
        s: PyStrRef,
        end: usize,
        strict: OptionalArg<bool>,
        vm: &VirtualMachine,
    ) -> PyResult<(Wtf8Buf, usize)> {
        flame_guard!("_json::scanstring");
        let wtf8 = s.as_wtf8();

        // Convert char index `end` to byte index
        let byte_idx = if end == 0 {
            0
        } else {
            wtf8.code_point_indices()
                .nth(end)
                .map(|(i, _)| i)
                .ok_or_else(|| {
                    py_decode_error(
                        machinery::DecodeError::new("Unterminated string starting at", end - 1),
                        s.clone(),
                        vm,
                    )
                })?
        };

        let (result, chars_consumed, _bytes_consumed) =
            machinery::scanstring(&wtf8[byte_idx..], end, strict.unwrap_or(true))
                .map_err(|e| py_decode_error(e, s, vm))?;

        Ok((result, end + chars_consumed))
    }
}
