"""Transparent zstd compression for chunk content.

Backward-compatible: reads detect the zstd magic byte sequence
(\\x28\\xb5\\x2f\\xfd) and decompress; plaintext rows from before this
feature was introduced pass through unchanged.

Why zstd over zlib: ~3-5x compression on natural language vs zlib's
~2-3x, and zstd decompresses ~5-10x faster than zlib. Level 3 is the
default — higher levels (5-9) gain only ~5-10% extra ratio at
significant compression-time CPU cost. Decompression speed is
level-independent (~250 MB/s on modern x86).

Storage in SQLite: declared TEXT columns happily hold BLOB cells thanks
to SQLite's dynamic typing. We don't change the schema; the column type
discipline is enforced at the application layer through these helpers.
"""

from __future__ import annotations

import zstandard

# Module-level singletons. The ZstdCompressor / ZstdDecompressor objects are
# stateless across calls — safe to share across threads in this codebase
# (we don't run multi-threaded ingests inside one process).
_COMPRESSOR = zstandard.ZstdCompressor(level=3)
_DECOMPRESSOR = zstandard.ZstdDecompressor()

# zstd frame magic (4 bytes). RFC 8478 §3.1.1.
_ZSTD_MAGIC = b"\x28\xb5\x2f\xfd"

# Below this UTF-8 byte length, compression overhead dominates and the
# compressed form is larger than the source. Empty / tiny chunks pass
# through as plaintext.
_MIN_COMPRESS_BYTES = 64


def is_compressed(value: object) -> bool:
    """True if value looks like a zstd frame (magic bytes match)."""
    return isinstance(value, (bytes, bytearray, memoryview)) and bytes(value[:4]) == _ZSTD_MAGIC


def pack_chunk(text: str) -> bytes | str:
    """Compress text for storage in `chunks.content`.

    Returns:
        bytes — zstd-compressed UTF-8 if compression saves space.
        str   — original text, unmodified, when too small to benefit.

    SQLite stores either as the column's natural cell type (TEXT for str,
    BLOB for bytes); both round-trip correctly through the Python sqlite3
    binding when read back.
    """
    if not text:
        return text
    raw = text.encode("utf-8")
    if len(raw) < _MIN_COMPRESS_BYTES:
        return text
    compressed = _COMPRESSOR.compress(raw)
    # Defensive: if the entropy is near-incompressible (already-compressed
    # data, very short repeats), keep the smaller representation.
    if len(compressed) >= len(raw):
        return text
    return compressed


def unpack_chunk(value: object) -> str | None:
    """Decompress a chunk content value if compressed; otherwise return as-is.

    Inputs:
        None       -> None (cold-tier or null content)
        str        -> str  (legacy plaintext rows)
        bytes-like -> str  (decoded UTF-8; decompressed first if zstd-framed)

    Raises TypeError on unexpected input shapes so a bad row surfaces loudly
    instead of silently returning garbage.
    """
    if value is None:
        return None
    if isinstance(value, str):
        return value
    if isinstance(value, (bytes, bytearray, memoryview)):
        b = bytes(value)
        if is_compressed(b):
            return _DECOMPRESSOR.decompress(b).decode("utf-8")
        # Legacy or non-compressed BLOB cell — try UTF-8 decode.
        return b.decode("utf-8")
    raise TypeError(
        f"unexpected chunk content type: {type(value).__name__}"
    )
