import hashlib
import json
import os
import tempfile
from copy import deepcopy
from pathlib import Path
from typing import Any

CONTENT_HASH_FIELD = "content_hash"


def _hashable_copy(data: Any, *, hash_field: str = CONTENT_HASH_FIELD) -> Any:
    if isinstance(data, dict):
        return {
            key: _hashable_copy(value, hash_field=hash_field)
            for key, value in data.items()
            if key != hash_field
        }
    if isinstance(data, list):
        return [_hashable_copy(item, hash_field=hash_field) for item in data]
    return data


def compute_json_content_hash(data: Any, *, hash_field: str = CONTENT_HASH_FIELD) -> str:
    payload = json.dumps(
        _hashable_copy(data, hash_field=hash_field),
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()


def attach_json_content_hash(data: Any, *, hash_field: str = CONTENT_HASH_FIELD) -> Any:
    cloned = deepcopy(data)
    if isinstance(cloned, dict):
        cloned[hash_field] = compute_json_content_hash(cloned, hash_field=hash_field)
    return cloned


def json_content_hash_matches(data: Any, *, hash_field: str = CONTENT_HASH_FIELD) -> bool:
    if not isinstance(data, dict):
        return True
    stored = str(data.get(hash_field, "")).strip()
    if not stored:
        return False
    return stored == compute_json_content_hash(data, hash_field=hash_field)


def write_json_atomic(path: Path, data: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
    temporary = Path(temporary_name)
    payload = attach_json_content_hash(data)
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
            json.dump(payload, handle, ensure_ascii=False, indent=2)
            handle.write("\n")
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def write_text_atomic(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
    temporary = Path(temporary_name)
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
            handle.write(content)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def read_json(path: Path) -> Any:
    with path.open("r", encoding="utf-8") as handle:
        return json.load(handle)
