#!/usr/bin/env python3
"""StartView 프로토타입 서버 — 정적 파일 + 공유 코멘트 API.

왜 한 프로세스인가:
  프로토타입은 이미 정적 파일로 서비스되고 있고, 필요한 건 코멘트를 여섯 명이
  같이 보는 것뿐이다. 별도 API 서버를 띄우면 포트·프록시·유닛이 하나씩 더 늘어난다.
  같은 프로세스에서 /api 만 가로채고 나머지는 그대로 파일로 내보낸다.

왜 SQLite 인가:
  이 서버에는 이미 Postgres 가 돌지만 그건 CRM 것이다. 리뷰용 코멘트 몇백 개
  때문에 남의 데이터베이스에 테이블을 얹을 이유가 없다. sqlite3 는 파이썬 표준
  라이브러리라 설치할 것도 없고, 파일 하나라 백업이 곧 복사다.

왜 통짜 배열 PUT 이 아닌가:
  클라이언트가 rows 전체를 저장하면 두 사람이 동시에 코멘트를 남길 때 나중 저장이
  앞사람 것을 지운다. 그래서 add/patch/delete 를 각각 받고, 서버가 진실을 돌려준다.
"""
import json
import os
import sqlite3
import threading
import time
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs

ROOT = os.environ.get("SV_ROOT", os.path.expanduser("~/prototype"))
DB_PATH = os.environ.get("SV_DB", os.path.join(ROOT, "comments.db"))
PORT = int(os.environ.get("SV_PORT", "8090"))
BIND = os.environ.get("SV_BIND", "127.0.0.1")

APPS = {"creator", "advertiser"}
MAX_BODY = 4 * 1024 * 1024          # 썸네일이 붙어도 넉넉하다
WRITE_LOCK = threading.Lock()


def connect():
    c = sqlite3.connect(DB_PATH, timeout=10)
    c.row_factory = sqlite3.Row
    return c


def init_db():
    with connect() as c:
        c.execute(
            """CREATE TABLE IF NOT EXISTS comments (
                 id   TEXT PRIMARY KEY,
                 app  TEXT NOT NULL,
                 at   INTEGER NOT NULL,
                 data TEXT NOT NULL
               )"""
        )
        c.execute("CREATE INDEX IF NOT EXISTS comments_app_at ON comments(app, at)")
        # 폴링이 매번 전체 행을 JSON 으로 만들지 않도록 리비전만 먼저 확인한다
        c.execute(
            """CREATE TABLE IF NOT EXISTS revs (
                 app TEXT PRIMARY KEY,
                 rev INTEGER NOT NULL
               )"""
        )


def bump(conn, app):
    conn.execute(
        "INSERT INTO revs(app, rev) VALUES(?, 1) "
        "ON CONFLICT(app) DO UPDATE SET rev = rev + 1",
        (app,),
    )


def read_rev(conn, app):
    row = conn.execute("SELECT rev FROM revs WHERE app=?", (app,)).fetchone()
    return row["rev"] if row else 0


def read_rows(conn, app):
    return [
        json.loads(r["data"])
        for r in conn.execute(
            "SELECT data FROM comments WHERE app=? ORDER BY at", (app,)
        )
    ]


class Handler(SimpleHTTPRequestHandler):
    server_version = "StartViewPrototype/1.0"

    def __init__(self, *a, **kw):
        super().__init__(*a, directory=ROOT, **kw)

    # ── helpers ──
    def _send_json(self, obj, code=200):
        body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def _read_json(self):
        n = int(self.headers.get("Content-Length") or 0)
        if n <= 0 or n > MAX_BODY:
            return None
        try:
            return json.loads(self.rfile.read(n).decode("utf-8"))
        except Exception:
            return None

    def _app(self, q):
        app = (q.get("app") or ["creator"])[0]
        return app if app in APPS else None

    def log_message(self, fmt, *args):
        # 정적 파일 요청까지 다 찍으면 journal 이 금방 찬다 — API 만 남긴다
        if "/api/" in (self.path or ""):
            super().log_message(fmt, *args)

    # ── routes ──
    def do_GET(self):
        u = urlparse(self.path)
        if u.path.startswith("/api/"):
            return self._api_get(u)
        return super().do_GET()

    def do_POST(self):
        u = urlparse(self.path)
        if u.path.startswith("/api/"):
            return self._api_post(u)
        self.send_error(405)

    def do_PATCH(self):
        u = urlparse(self.path)
        if u.path.startswith("/api/"):
            return self._api_patch(u)
        self.send_error(405)

    def do_DELETE(self):
        u = urlparse(self.path)
        if u.path.startswith("/api/"):
            return self._api_delete(u)
        self.send_error(405)

    # ── api ──
    def _api_get(self, u):
        q = parse_qs(u.query)
        app = self._app(q)
        if app is None:
            return self._send_json({"error": "unknown app"}, 400)

        if u.path == "/api/health":
            return self._send_json({"ok": True, "at": int(time.time() * 1000)})

        if u.path != "/api/comments":
            return self._send_json({"error": "not found"}, 404)

        with connect() as c:
            rev = read_rev(c, app)
            # 클라이언트가 알고 있는 리비전과 같으면 본문을 만들지 않는다
            since = (q.get("rev") or [None])[0]
            if since is not None and since.isdigit() and int(since) == rev:
                return self._send_json({"rev": rev, "unchanged": True})
            return self._send_json({"rev": rev, "rows": read_rows(c, app)})

    def _api_post(self, u):
        q = parse_qs(u.query)
        app = self._app(q)
        if app is None or u.path != "/api/comments":
            return self._send_json({"error": "bad request"}, 400)
        row = self._read_json()
        if not isinstance(row, dict) or not row.get("id"):
            return self._send_json({"error": "bad row"}, 400)
        with WRITE_LOCK, connect() as c:
            c.execute(
                "INSERT OR REPLACE INTO comments(id, app, at, data) VALUES(?,?,?,?)",
                (row["id"], app, int(row.get("at") or time.time() * 1000),
                 json.dumps(row, ensure_ascii=False)),
            )
            bump(c, app)
            return self._send_json({"rev": read_rev(c, app), "rows": read_rows(c, app)})

    def _api_patch(self, u):
        q = parse_qs(u.query)
        app = self._app(q)
        if app is None or not u.path.startswith("/api/comments/"):
            return self._send_json({"error": "bad request"}, 400)
        cid = u.path.rsplit("/", 1)[-1]
        patch = self._read_json()
        if not isinstance(patch, dict):
            return self._send_json({"error": "bad patch"}, 400)
        with WRITE_LOCK, connect() as c:
            cur = c.execute(
                "SELECT data FROM comments WHERE id=? AND app=?", (cid, app)
            ).fetchone()
            if not cur:
                return self._send_json({"error": "not found"}, 404)
            row = json.loads(cur["data"])
            row.update(patch)
            c.execute(
                "UPDATE comments SET data=? WHERE id=? AND app=?",
                (json.dumps(row, ensure_ascii=False), cid, app),
            )
            bump(c, app)
            return self._send_json({"rev": read_rev(c, app), "rows": read_rows(c, app)})

    def _api_delete(self, u):
        q = parse_qs(u.query)
        app = self._app(q)
        if app is None or not u.path.startswith("/api/comments/"):
            return self._send_json({"error": "bad request"}, 400)
        cid = u.path.rsplit("/", 1)[-1]
        with WRITE_LOCK, connect() as c:
            c.execute("DELETE FROM comments WHERE id=? AND app=?", (cid, app))
            bump(c, app)
            return self._send_json({"rev": read_rev(c, app), "rows": read_rows(c, app)})


if __name__ == "__main__":
    init_db()
    srv = ThreadingHTTPServer((BIND, PORT), Handler)
    print(f"serving {ROOT} on {BIND}:{PORT}  (db: {DB_PATH})", flush=True)
    srv.serve_forever()
