"""State persistence for the scaffold core.

read_state  — tolerant read: missing -> None, corrupt -> None + log, valid -> value.
write_state — process-crash-safe write (temp + os.replace); unserialisable/IO failure -> False + log.

Never raises. The scaffold threads state between ticks via these two functions.
"""

import json
import os
import tempfile

from .loop_primitives import log_event


def read_state(state_path: str):
    """Read the JSON state file at state_path.

    Returns:
        The parsed JSON value, or None if:
          - the file does not exist (silently), or
          - the file is empty, corrupt, or truncated (logs scaffold_state_corrupt).
        JSON null in the file is returned as Python None (indistinguishable from
        "no state" — both mean "start fresh").

    The file is never deleted or modified by this function.
    """
    if not os.path.exists(state_path):
        return None

    try:
        with open(state_path, "r", encoding="utf-8") as fh:
            content = fh.read()
    except OSError as exc:
        log_event("scaffold_state_corrupt", state_path=state_path, reason=str(exc))
        return None

    if not content.strip():
        log_event("scaffold_state_corrupt", state_path=state_path, reason="empty file")
        return None

    try:
        return json.loads(content)
    except json.JSONDecodeError as exc:
        log_event(
            "scaffold_state_corrupt",
            state_path=state_path,
            reason=f"JSON parse error: {exc}",
        )
        return None


def write_state(state_path: str, new_state) -> bool:
    """Write new_state as JSON to state_path, replacing it atomically.

    Uses a temp file in the same directory and os.replace. This is atomic
    with respect to a process crash (container SIGKILL): a reader always
    sees the complete old file or the complete new file, never a partial
    write. However, neither the temp fd nor the directory is fsynced before
    the rename, so the guarantee does NOT extend to an OS crash or power
    loss — in that case the target file may be zero-length or partial.

    Returns:
        True on success.
        False (never raises) on:
          - unserialisable new_state (logs scaffold_state_unserialisable, prior file intact).
          - IO failure, e.g. read-only directory (logs, prior file intact).
    """
    # Serialise first — before touching the filesystem — so a bad value never
    # creates a partial temp file at the destination directory.
    try:
        serialised = json.dumps(new_state)
    except (TypeError, ValueError) as exc:
        log_event(
            "scaffold_state_unserialisable",
            state_path=state_path,
            reason=f"{type(exc).__name__}: {exc}",
        )
        return False

    # Write to a temp file in the same directory so os.replace is atomic
    # (same filesystem, no cross-device rename).
    dir_path = os.path.dirname(os.path.abspath(state_path))
    tmp_fd = None
    tmp_path = None
    try:
        tmp_fd, tmp_path = tempfile.mkstemp(dir=dir_path, suffix=".tmp")
        try:
            os.write(tmp_fd, serialised.encode("utf-8"))
        finally:
            os.close(tmp_fd)
            tmp_fd = None
        os.replace(tmp_path, state_path)
        tmp_path = None  # successfully replaced; don't clean up
        return True
    except OSError as exc:
        log_event(
            "scaffold_state_write_failed",
            state_path=state_path,
            reason=f"{type(exc).__name__}: {exc}",
        )
        return False
    finally:
        # Clean up the temp file if it was not consumed by os.replace.
        if tmp_fd is not None:
            try:
                os.close(tmp_fd)
            except OSError:
                pass
        if tmp_path is not None:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass
