"""Shared, safe access to discourse.json for the agents and the forum server.

Every write goes through update(), which locks the file across processes, re-reads it,
applies the change and swaps the new file in. Agents and the web UI can then write
at the same time without losing each other's posts or corrupting the file.
"""
import json
import os
import re
import tempfile
import time
from contextlib import contextmanager
from pathlib import Path

PATH = Path(__file__).with_name("discourse.json")
LOCK = PATH.with_name("discourse.json.lock")


@contextmanager
def file_lock(stale_after=30):
	while True:
		try:
			fd = os.open(LOCK, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
			break
		except FileExistsError:
			try:  # clear a lock left behind by a crashed process
				if time.time() - os.path.getmtime(LOCK) > stale_after:
					os.remove(LOCK)
			except FileNotFoundError:
				pass
			time.sleep(0.05)
	try:
		yield
	finally:
		os.close(fd)
		try:
			os.remove(LOCK)
		except FileNotFoundError:
			pass


def load():
	with open(PATH, encoding="utf-8") as f:
		return json.load(f)


def _save(data):
	fd, tmp = tempfile.mkstemp(dir=PATH.parent, suffix=".tmp")
	with os.fdopen(fd, "w", encoding="utf-8") as f:
		json.dump(data, f, indent=4, ensure_ascii=False)
	for _ in range(20):  # Windows/OneDrive can briefly hold the file open
		try:
			os.replace(tmp, PATH)
			return
		except PermissionError:
			time.sleep(0.1)
	os.remove(tmp)
	raise RuntimeError("Could not save discourse.json (file is locked)")


def update(change):
	"""Lock, re-read, apply change(data), save. Returns (new_data, change's return value).
	If change raises, nothing is saved."""
	with file_lock():
		data = load()
		result = change(data)
		_save(data)
	return data, result


def next_id(d, prefix="", width=4):
	"""Next sequential ID. Handles both "0012" (comments) and "t007" (threads)."""
	nums = [int(m.group()) for k in d if (m := re.search(r"\d+", k))]
	return f"{prefix}{max(nums, default=-1) + 1:0{width}d}"