"""Abstract storage backends and JSON file implementation."""

from __future__ import annotations

import asyncio
import json
import logging
import os
from pathlib import Path
from typing import Any, Protocol, runtime_checkable

_LOGGER = logging.getLogger(__name__)


@runtime_checkable
class StorageBackend(Protocol):
    """Abstract storage backend for persisting JSON-serializable data."""

    async def load(self) -> dict[str, Any] | None: ...
    async def save(self, data: dict[str, Any]) -> None: ...
    async def remove(self) -> None: ...


def _read_file(path: Path) -> str | None:
    if not path.exists():
        return None
    return path.read_text(encoding="utf-8")


def _atomic_write(path: Path, payload: str) -> None:
    """Write atomically: tmp file + rename. Prevents half-written files on crash."""
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(payload, encoding="utf-8")
    os.replace(tmp, path)


def _remove_file(path: Path) -> None:
    if path.exists():
        path.unlink()


class JsonFileBackend:
    """Simple JSON file storage backend.

    All blocking I/O is wrapped in ``asyncio.to_thread`` so callers can
    ``await load() / save() / remove()`` without stalling the event loop.
    """

    def __init__(self, path: Path) -> None:
        self._path = path

    async def load(self) -> dict[str, Any] | None:
        try:
            text = await asyncio.to_thread(_read_file, self._path)
        except OSError as err:
            _LOGGER.warning("Failed to load %s: %s", self._path, err)
            return None
        if text is None:
            return None
        try:
            return json.loads(text)
        except json.JSONDecodeError as err:
            _LOGGER.warning("Failed to parse %s: %s", self._path, err)
            return None

    async def save(self, data: dict[str, Any]) -> None:
        payload = json.dumps(data, default=str)
        try:
            await asyncio.to_thread(_atomic_write, self._path, payload)
        except OSError as err:
            _LOGGER.error("Failed to save %s: %s", self._path, err)

    async def remove(self) -> None:
        try:
            await asyncio.to_thread(_remove_file, self._path)
        except OSError as err:
            _LOGGER.error("Failed to remove %s: %s", self._path, err)
