"""File I/O operations for the Crous binary format.

Provides convenience functions for reading and writing `.crous` files
directly from/to disk, mirroring the `json.load()` / `json.dump()` API.

Quick start::

    import crous

    # Write a Python object to a .crous file
    crous.dump({"name": "Alice", "age": 30}, "data.crous")

    # Read it back
    obj = crous.load("data.crous")
    print(obj)  # {'name': 'Alice', 'age': 30}

    # Work with Value objects for lossless round-trips
    values = crous.load_values("data.crous")
    crous.dump_values(values, "data_copy.crous")
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Union

from crous.decoder import Decoder
from crous.encoder import Encoder
from crous.value import Value

# Type alias for path-like arguments
PathLike = Union[str, bytes, os.PathLike]


def load(path: PathLike) -> object:
    """Read and decode a `.crous` file, returning native Python objects.

    Returns a single Python value if the file contains one top-level value,
    or a list of values otherwise. Returns ``None`` for an empty file.

    This is the Crous equivalent of ``json.load()``.

    Args:
        path: Path to the `.crous` file to read.

    Returns:
        The decoded Python object(s).

    Raises:
        FileNotFoundError: If the file does not exist.
        CrousError: If the file is corrupted or invalid.

    Example::

        >>> import crous
        >>> crous.dump({"name": "Alice"}, "test.crous")
        >>> crous.load("test.crous")
        {'name': 'Alice'}
    """
    data = Path(path).read_bytes()
    dec = Decoder(data)
    values = dec.decode_all()
    if not values:
        return None
    if len(values) == 1:
        return values[0].to_python()
    return [v.to_python() for v in values]


def load_values(path: PathLike) -> list[Value]:
    """Read and decode a `.crous` file, returning ``Value`` objects.

    Unlike ``load()``, this preserves the Crous type information
    (e.g., distinguishing ``UInt`` from ``Int``), enabling lossless
    round-trips through encode/decode.

    Args:
        path: Path to the `.crous` file to read.

    Returns:
        A list of ``Value`` objects.

    Raises:
        FileNotFoundError: If the file does not exist.
        CrousError: If the file is corrupted or invalid.

    Example::

        >>> import crous
        >>> crous.dump(42, "num.crous")
        >>> values = crous.load_values("num.crous")
        >>> values[0].type
        <ValueType.UINT: ...>
    """
    data = Path(path).read_bytes()
    dec = Decoder(data)
    return dec.decode_all()


def dump(obj: object, path: PathLike) -> int:
    """Encode a Python object and write it to a `.crous` file.

    Creates the file if it does not exist, or overwrites it if it does.

    This is the Crous equivalent of ``json.dump()``.

    Args:
        obj: The Python object to encode (dict, list, str, int, float,
             bool, None, bytes, or bytearray).
        path: Path to the output `.crous` file.

    Returns:
        The number of bytes written.

    Raises:
        CrousError: If the object cannot be encoded.

    Example::

        >>> import crous
        >>> n = crous.dump({"users": ["Alice", "Bob"]}, "users.crous")
        >>> n > 8  # At least header + block
        True
    """
    enc = Encoder()
    enc.encode_value(Value.from_python(obj))
    data = enc.finish()
    p = Path(path)
    p.write_bytes(data)
    return len(data)


def dump_values(values: list[Value], path: PathLike) -> int:
    """Encode ``Value`` objects and write them to a `.crous` file.

    This preserves exact Crous type information for lossless round-trips.

    Args:
        values: A list of ``Value`` objects to encode.
        path: Path to the output `.crous` file.

    Returns:
        The number of bytes written.

    Raises:
        CrousError: If a value cannot be encoded.

    Example::

        >>> import crous
        >>> from crous.value import Value
        >>> vals = [Value.str_("hello"), Value.uint(42)]
        >>> crous.dump_values(vals, "multi.crous")
        ...
    """
    enc = Encoder()
    for v in values:
        enc.encode_value(v)
    data = enc.finish()
    p = Path(path)
    p.write_bytes(data)
    return len(data)


def append(obj: object, path: PathLike) -> int:
    """Encode a Python object and append it to an existing `.crous` file.

    If the file does not exist, a new file is created. If it does exist,
    the new value is appended after the existing data (the file is
    re-encoded with all values combined).

    For high-throughput streaming appends, consider using the ``Encoder``
    class directly with incremental I/O.

    Args:
        obj: The Python object to append.
        path: Path to the `.crous` file.

    Returns:
        The number of bytes in the resulting file.

    Raises:
        CrousError: If the object cannot be encoded or the file is corrupted.

    Example::

        >>> import crous
        >>> crous.dump({"event": "start"}, "log.crous")
        ...
        >>> crous.append({"event": "stop"}, "log.crous")
        ...
        >>> crous.load("log.crous")
        [{'event': 'start'}, {'event': 'stop'}]
    """
    p = Path(path)
    if p.exists():
        existing = load_values(p)
    else:
        existing = []
    existing.append(Value.from_python(obj))
    return dump_values(existing, p)
