"""Serve the forum locally, open it in the browser, and save posts made in the UI.

Run:  python serve.py
Keep forum.html, discourse.json, forum_store.py and this file in the same folder.
Use this instead of `python -m http.server`, which can't save anything.
"""
import http.server
import json
import os
import webbrowser

from forum_store import update, next_id

HERE = os.path.dirname(os.path.abspath(__file__))
START_PORT = 8000


def add_comment(thread_id, parent_id, user, body):
	def change(d):
		if not str(body).strip():
			raise ValueError("comment is empty")
		if thread_id not in d["threads"]:
			raise ValueError(f"thread {thread_id} doesn't exist")
		pid = parent_id or "n/a"
		if not any(c["thread_id"] == thread_id for c in d["comments"].values()):
			pid = "n/a"
		elif pid != "n/a":
			if pid not in d["comments"]:
				raise ValueError(f"parent comment {pid} doesn't exist")
			if d["comments"][pid]["thread_id"] != thread_id:
				raise ValueError(f"parent comment {pid} is in a different thread")
		cid = next_id(d["comments"], width=4)
		d["comments"][cid] = {"thread_id": thread_id, "parent_id": pid, "user": user, "body": body}
		return {"comment_id": cid, "thread_id": thread_id}
	return update(change)[1]


def add_thread(category, title, user, body=None):
	def change(d):
		if not str(title).strip():
			raise ValueError("title is empty")
		if category not in d["categories"]:
			raise ValueError(f"category {category} doesn't exist")
		tid = next_id(d["threads"], prefix="t", width=3)
		d["threads"][tid] = {"category": category, "title": title.strip(), "author": user}
		result = {"thread_id": tid}
		if body and body.strip():  # optional text becomes the first comment
			cid = next_id(d["comments"], width=4)
			d["comments"][cid] = {"thread_id": tid, "parent_id": "n/a", "user": user, "body": body}
			result["comment_id"] = cid
		return result
	return update(change)[1]


class Handler(http.server.SimpleHTTPRequestHandler):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, directory=HERE, **kwargs)

	def end_headers(self):
		self.send_header("Cache-Control", "no-store")
		super().end_headers()

	def log_message(self, *args):  # keep the console quiet during polling
		pass

	def _json(self, status, obj):
		data = json.dumps(obj).encode("utf-8")
		self.send_response(status)
		self.send_header("Content-Type", "application/json")
		self.send_header("Content-Length", str(len(data)))
		self.end_headers()
		self.wfile.write(data)

	def do_POST(self):
		# Only accept JSON from this page; blocks other websites from posting to it.
		if "application/json" not in self.headers.get("Content-Type", ""):
			return self._json(415, {"error": "expected application/json"})
		origin = self.headers.get("Origin")
		if origin and origin.split("//", 1)[-1] != self.headers.get("Host"):
			return self._json(403, {"error": "cross-origin request rejected"})
		try:
			length = int(self.headers.get("Content-Length", 0))
			p = json.loads(self.rfile.read(length) or b"{}")
			user = str(p.get("user") or "Secretary")
			if self.path == "/api/comment":
				result = add_comment(p["thread_id"], p.get("parent_id", "n/a"), user, str(p["body"]))
			elif self.path == "/api/thread":
				result = add_thread(p["category"], str(p["title"]), user, p.get("body"))
			else:
				return self._json(404, {"error": "unknown endpoint"})
			print(f"[UI] {user} -> {result}")
			self._json(200, result)
		except (ValueError, KeyError) as e:
			self._json(400, {"error": str(e)})
		except Exception as e:
			self._json(500, {"error": f"{type(e).__name__}: {e}"})


def main():
	for port in range(START_PORT, START_PORT + 20):
		try:
			server = http.server.ThreadingHTTPServer(("127.0.0.1", port), Handler)
			break
		except OSError:
			continue
	else:
		raise SystemExit("No free port found between 8000 and 8019.")

	url = f"http://localhost:{port}/forum.html"
	print(f"Forum running at {url}  (Ctrl+C to stop)")
	webbrowser.open(url)
	try:
		server.serve_forever()
	except KeyboardInterrupt:
		print("\nStopped.")


if __name__ == "__main__":
	main()