"""
Decode/encode the "MBD" build-share wire format Mids Reborn uses for its
clipboard export/import ("Export build" / "Import build").

Mirrors, field-for-field:
  MidsReborn/Core/Utils/DataClassifier.cs   (header regex, format detection)
  MidsReborn/Core/BuildFile/BuildManager.cs (ValidateAndLoadImportData - the
                                              actual header/body split + decode)
  MidsReborn/Core/Utils/Compression.cs      (base64 + Brotli)
  MidsReborn/Core/Utils/ModernZlib.cs       (BreakString/UnbreakString line wrapping)
  MidsReborn/Core/BuildFile/CharacterBuildData.cs (GenerateChunkData - the encoder)

Wire format:
  |MBD;<uncompressedSize>;<compressedSize>;<encodedSize>;BASE64;|
  <base64 body, wrapped at 67 chars/line, each line bookended with "|">

The body's base64 decodes to Brotli-compressed UTF-8 JSON text matching the
CharacterBuildData schema (see build_model.py).

A bare base64 blob with no header (Mids' "UnkBase64" case - e.g. some
share-link payloads) is also supported: same base64 -> Brotli -> JSON
pipeline, just with no declared sizes to sanity-check against.

The older "|MxDz;...;HEX;|" format (custom zlib + hex, pre-Brotli) is
explicitly NOT supported here - lower priority per docs/research-data-sources.md,
only needed for very old forum-shared builds.
"""
import base64
import json
import re

from brotli_ctypes import compress as brotli_compress, decompress as brotli_decompress

MBD_HEADER_RE = re.compile(r"^\|MBD;([0-9]+);([0-9]+);([0-9]+);BASE64;\|")
MXDZ_HEADER_RE = re.compile(r"^\|MxDz;([0-9]+);([0-9]+);([0-9]+);HEX;\|")
UNK_BASE64_RE = re.compile(r"^[A-Za-z0-9+/]+={0,2}$")

LINE_WRAP = 67


class MbdFormatError(ValueError):
    pass


def classify(text):
    """Mirrors DataClassifier.ClassifyAndExtractData. Returns one of
    "mbd", "mxdz", "unk_base64", or raises MbdFormatError for unrecognized
    input (Mids' "Unknown" case)."""
    stripped = text.strip()
    if MBD_HEADER_RE.match(stripped):
        return "mbd"
    if MXDZ_HEADER_RE.match(stripped):
        return "mxdz"
    if UNK_BASE64_RE.match(stripped):
        return "unk_base64"
    raise MbdFormatError("Unrecognized build data format (not MBD, MxDz, or bare base64).")


def decode_mbd_text(text):
    """Full decode: pasted clipboard text -> parsed CharacterBuildData dict.
    Mirrors BuildManager.ValidateAndLoadImportData's Mbd/UnkBase64 branches."""
    kind = classify(text)

    if kind == "mxdz":
        raise MbdFormatError(
            "Legacy MxDz format (pre-Brotli, hex-encoded) is not supported. "
            "Re-export the build from a current Mids Reborn as MBD."
        )

    if kind == "unk_base64":
        data = text.strip()
        decoded_bytes = base64.b64decode(data)
        json_bytes = brotli_decompress(decoded_bytes)
        return json.loads(json_bytes.decode("utf-8"))

    # kind == "mbd"
    # Mirrors: lines = content.Split(['\r','\n'], RemoveEmptyEntries)
    lines = [line for line in re.split(r"[\r\n]+", text.strip()) if line]
    if len(lines) < 2:
        raise MbdFormatError("Input format is incorrect. No data found after header.")

    header = lines[0].strip("|").strip()
    header_items = [h for h in header.split(";") if h]
    if len(header_items) != 5:
        raise MbdFormatError("Header format is incorrect.")

    try:
        uncompressed_size = int(header_items[1])
        compressed_size = int(header_items[2])
    except ValueError as exc:
        raise MbdFormatError("Header contains invalid size information.") from exc

    # Mirrors: data = string.Join("", lines.Skip(1)).Replace("|", "")
    data = "".join(lines[1:]).replace("|", "")

    if not re.match(r"^[a-zA-Z0-9+/]*={0,3}$", data):
        raise MbdFormatError("Data is not valid BASE64.")

    compressed_bytes = base64.b64decode(data)
    if len(compressed_bytes) != compressed_size:
        raise MbdFormatError(
            f"Compressed size mismatch: header says {compressed_size}, got {len(compressed_bytes)}."
        )

    json_bytes = brotli_decompress(compressed_bytes, size_hint=uncompressed_size)
    if len(json_bytes) != uncompressed_size:
        # Not fatal on its own (Mids doesn't check this), but worth surfacing.
        pass

    return json.loads(json_bytes.decode("utf-8"))


def encode_mbd_text(data):
    """Reverse of decode_mbd_text's "mbd" branch. Mirrors
    CharacterBuildData.GenerateChunkData(): JSON (compact, matching
    Newtonsoft's Formatting.None) -> UTF-8 -> Brotli -> base64 -> line-wrapped
    |MBD;...;| envelope."""
    serialized = json.dumps(data, separators=(",", ":"))
    raw_bytes = serialized.encode("utf-8")
    compressed_bytes = brotli_compress(raw_bytes)
    encoded = base64.b64encode(compressed_bytes).decode("ascii")

    body = _break_string(encoded, LINE_WRAP, bookend=True)
    header = f"|MBD;{len(raw_bytes)};{len(compressed_bytes)};{len(encoded)};BASE64;|"
    return f"{header}\r\n{body}"


def _break_string(s, length, bookend=False):
    """Mirrors ModernZlib.BreakString."""
    lines = []
    for i in range(0, len(s), length):
        chunk = s[i : i + length]
        lines.append(f"|{chunk}|" if bookend else chunk)
    return "\r\n".join(lines)


if __name__ == "__main__":
    sample = {"Level": "50", "Class": "Class_Blaster", "PowerSets": [], "PowerEntries": []}
    encoded = encode_mbd_text(sample)
    print(encoded[:120], "...")
    decoded = decode_mbd_text(encoded)
    print("round-trip ok:", decoded == sample)
