"""Atomic JSON persistence for the ``.zyte`` stores."""

from __future__ import annotations

import json
import os
import posixpath
import tempfile
from pathlib import Path
from typing import Any

STORE_DIRNAME = ".zyte"
INFO_GRAPH_NAME = "info_graph.json"
ACTION_GRAPH_NAME = "chat_action_graph.json"
CONTEXT_STORE_NAME = "context-store.json"
ANSWER_LEDGER_NAME = "answer_ledger.json"


def normalize_root(root: str | os.PathLike[str]) -> Path:
    path = Path(root).expanduser()
    try:
        return path.resolve()
    except OSError:
        return path.absolute()


def store_dir(root: str | os.PathLike[str]) -> Path:
    return normalize_root(root) / STORE_DIRNAME


def info_graph_path(root: str | os.PathLike[str]) -> Path:
    return store_dir(root) / INFO_GRAPH_NAME


def action_graph_path(root: str | os.PathLike[str]) -> Path:
    return store_dir(root) / ACTION_GRAPH_NAME


def context_store_path(root: str | os.PathLike[str]) -> Path:
    return store_dir(root) / CONTEXT_STORE_NAME


def answer_ledger_path(root: str | os.PathLike[str]) -> Path:
    return store_dir(root) / ANSWER_LEDGER_NAME


def to_rel(root: Path, path: str) -> str:
    """Repo-relative posix form of ``path``, which may be absolute or already relative."""
    text = str(path).replace("\\", "/").strip()
    if not text:
        return ""
    probe = Path(text)
    if probe.is_absolute():
        try:
            return probe.resolve().relative_to(root).as_posix()
        except (OSError, ValueError):
            return probe.as_posix().lstrip("/")
    normalized = posixpath.normpath(text)
    if normalized.startswith("./"):
        normalized = normalized[2:]
    return "" if normalized == "." else normalized


def parse_ref(root: Path, ref: str) -> tuple[str, str | None]:
    """Split ``"path"`` or ``"path::symbol"`` into a repo-relative path and symbol."""
    text = str(ref)
    path, separator, symbol = text.partition("::")
    return to_rel(root, path), (symbol.strip() or None) if separator else None


def read_json(path: Path) -> dict[str, Any] | None:
    """Return the mapping stored at ``path``, or None when absent or corrupt."""
    try:
        with open(path, encoding="utf-8") as handle:
            payload = json.load(handle)
    except (OSError, ValueError):
        return None
    return payload if isinstance(payload, dict) else None


def write_json(path: Path, payload: dict[str, Any]) -> bool:
    """Serialize ``payload`` to ``path`` via a same-directory temp file and rename."""
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        handle, tmp_name = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")
    except OSError:
        return False
    tmp = Path(tmp_name)
    try:
        with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
            json.dump(payload, stream, ensure_ascii=False)
            stream.flush()
            os.fsync(stream.fileno())
        os.replace(tmp, path)
        return True
    except (OSError, TypeError, ValueError):
        try:
            tmp.unlink()
        except OSError:
            pass
        return False
