"""Fixed, bounded CPython ``re`` bridge used by the Python engine worker.

This file is bundled as source by Vite and evaluated once while the trusted
Pyodide runtime starts. User-controlled values are passed as function
arguments; they are never evaluated as Python source.
"""

import base64
import json
import re
import sys


_BRIDGE_ABI_VERSION = 1
_MAX_CAPTURE_GROUPS = 1_000
_MAX_NATIVE_EXPANSION_CODE_POINTS = 16 * 1024 * 1024


def _json_result(value):
    return json.dumps(value, ensure_ascii=True, separators=(",", ":"))


def _error(phase, code, error, position=None):
    message = str(error)
    if len(message) > 1_024:
        message = message[:1_023] + "\N{HORIZONTAL ELLIPSIS}"
    result = {
        "abi": _BRIDGE_ABI_VERSION,
        "ok": False,
        "phase": phase,
        "code": code,
        "message": message,
    }
    if isinstance(position, int) and position >= 0:
        result["position"] = position
    return _json_result(result)


def _flag_bits(flags):
    bits = 0
    if "a" in flags:
        bits |= re.ASCII
    if "i" in flags:
        bits |= re.IGNORECASE
    if "m" in flags:
        bits |= re.MULTILINE
    if "s" in flags:
        bits |= re.DOTALL
    if "x" in flags:
        bits |= re.VERBOSE
    return bits


def _match_record(match, group_count):
    captures = []
    for group_number in range(1, group_count + 1):
        start, end = match.span(group_number)
        captures.append(None if start < 0 else [start, end])
    start, end = match.span(0)
    return {"span": [start, end], "captures": captures}


def _utf8_width(character):
    code_point = ord(character)
    if code_point <= 0x7F:
        return 1
    if code_point <= 0x7FF:
        return 2
    if code_point <= 0xFFFF:
        return 3
    return 4


def _append_utf8_bounded(chunks, value, byte_count, maximum_bytes):
    remaining = maximum_bytes - byte_count
    if not value:
        return byte_count, True
    end = 0
    for character in value:
        width = _utf8_width(character)
        if width > remaining:
            break
        remaining -= width
        byte_count += width
        end += 1
    if end:
        chunks.append(value[:end])
    return byte_count, end == len(value)


def _expansion_code_points(parsed_template, match):
    total = 0
    for part in parsed_template:
        if isinstance(part, int):
            start, end = match.span(part)
            if start >= 0:
                total += end - start
        else:
            total += len(part)
        if total > _MAX_NATIVE_EXPANSION_CODE_POINTS:
            return total
    return total


def _identity():
    return _json_result(
        {
            "abi": _BRIDGE_ABI_VERSION,
            "ok": True,
            "identity": {
                "implementation": sys.implementation.name,
                "pythonVersion": ".".join(
                    str(part) for part in sys.version_info[:3]
                ),
                "reModule": re.__name__,
            },
        }
    )


def _regex_tools_run(
    operation,
    pattern,
    flags,
    subject,
    maximum_matches,
    maximum_capture_rows,
    replacement,
    maximum_output_bytes,
):
    if operation == "identity":
        return _identity()
    if operation not in ("execute", "replace"):
        return _error("bridge", "invalid-operation", "Unsupported bridge operation.")

    try:
        compiled = re.compile(pattern, _flag_bits(flags))
    except re.PatternError as error:
        return _error(
            "compile",
            "compile-error",
            error,
            getattr(error, "pos", None),
        )
    except Exception as error:
        return _error("compile", "compile-error", error)

    group_count = compiled.groups
    if group_count > _MAX_CAPTURE_GROUPS:
        return _error(
            "compile",
            "capture-group-limit",
            (
                f"The pattern declares {group_count} capture groups; "
                f"the bridge limit is {_MAX_CAPTURE_GROUPS}."
            ),
        )

    group_names = sorted(
        ([group_number, name] for name, group_number in compiled.groupindex.items()),
        key=lambda item: item[0],
    )

    parsed_template = None
    if operation == "replace":
        try:
            parsed_template = re._parser.parse_template(replacement, compiled)
        except (re.PatternError, IndexError) as error:
            return _error(
                "replacement",
                "replacement-error",
                error,
                getattr(error, "pos", None),
            )
        except Exception as error:
            return _error("replacement", "replacement-error", error)

    iterate = "g" in flags
    try:
        matches = compiled.finditer(subject) if iterate else (
            candidate for candidate in (compiled.search(subject),) if candidate
        )
        records = []
        capture_rows = 0
        results_truncated = False
        output_chunks = []
        output_bytes = 0
        output_truncated = False
        output_stopped = False
        next_source_position = 0

        for match in matches:
            rows_for_match = max(1, group_count)
            if (
                len(records) >= maximum_matches
                or capture_rows + rows_for_match > maximum_capture_rows
            ):
                results_truncated = True
                break

            records.append(_match_record(match, group_count))
            capture_rows += rows_for_match

            if operation == "replace" and not output_stopped:
                start, end = match.span(0)
                output_bytes, complete = _append_utf8_bounded(
                    output_chunks,
                    subject[next_source_position:start],
                    output_bytes,
                    maximum_output_bytes,
                )
                if not complete:
                    output_truncated = True
                    output_stopped = True
                else:
                    expansion_size = _expansion_code_points(
                        parsed_template, match
                    )
                    if expansion_size > _MAX_NATIVE_EXPANSION_CODE_POINTS:
                        return _error(
                            "replacement",
                            "replacement-expansion-limit",
                            (
                                "One native match expansion exceeds the "
                                f"{_MAX_NATIVE_EXPANSION_CODE_POINTS} "
                                "code-point safety limit."
                            ),
                        )
                    expanded = match.expand(replacement)
                    output_bytes, complete = _append_utf8_bounded(
                        output_chunks,
                        expanded,
                        output_bytes,
                        maximum_output_bytes,
                    )
                    if not complete:
                        output_truncated = True
                        output_stopped = True
                next_source_position = end

        if operation == "replace" and not output_stopped:
            output_bytes, complete = _append_utf8_bounded(
                output_chunks,
                subject[next_source_position:],
                output_bytes,
                maximum_output_bytes,
            )
            if not complete:
                output_truncated = True

        result = {
            "abi": _BRIDGE_ABI_VERSION,
            "ok": True,
            "groupCount": group_count,
            "groupNames": group_names,
            "matches": records,
            "resultsTruncated": results_truncated,
        }
        if operation == "replace":
            output = "".join(output_chunks).encode("utf-8")
            if len(output) != output_bytes:
                return _error(
                    "bridge",
                    "output-byte-count-mismatch",
                    "The bounded Python output byte count is inconsistent.",
                )
            result.update(
                {
                    "outputBase64": base64.b64encode(output).decode("ascii"),
                    "outputBytes": output_bytes,
                    "outputTruncated": output_truncated,
                }
            )
        return _json_result(result)
    except re.PatternError as error:
        phase = "replacement" if operation == "replace" else "match"
        code = "replacement-error" if operation == "replace" else "match-error"
        return _error(phase, code, error, getattr(error, "pos", None))
    except Exception as error:
        phase = "replacement" if operation == "replace" else "match"
        code = "replacement-error" if operation == "replace" else "match-error"
        return _error(phase, code, error)


_regex_tools_run
