import { Server } from "@hocuspocus/server"
import { TiptapTransformer } from "@hocuspocus/transformer"
import * as Y from "yjs"
import { createRequire } from "node:module"

const require = createRequire(import.meta.url)

const StarterKit = require("@tiptap/starter-kit").default

const PORT = Number(process.env.HOCUSPOCUS_PORT || 1234)
const SECRET = process.env.HOCUSPOCUS_SECRET || ""
const ULTID_URL = (process.env.ULTID_INTERNAL_URL || "http://127.0.0.1").replace(/\/$/, "")

function decodeJwtPayload(token) {
  const parts = token.split(".")
  if (parts.length < 2) return null
  try {
    const json = Buffer.from(parts[1], "base64url").toString("utf8")
    return JSON.parse(json)
  } catch {
    return null
  }
}

function hookContext(payload) {
  return payload?.lastContext ?? payload?.context ?? {}
}

async function loadFromUltid(context) {
  if (!context?.path || !context?.user) return null
  const params = new URLSearchParams({ user: context.user, path: context.path })
  const res = await fetch(`${ULTID_URL}/api/v1/richtext/internal/document?${params}`, {
    headers: SECRET ? { "X-Hocuspocus-Secret": SECRET } : {},
  })
  if (res.status === 404) return null
  if (!res.ok) throw new Error(`load failed: ${res.status}`)
  const raw = await res.text()
  if (!raw.trim()) return null
  try {
    const doc = JSON.parse(raw)
    if (doc.yjsState) {
      return Buffer.from(doc.yjsState, "base64")
    }
    if (doc.content) {
      const ydoc = TiptapTransformer.toYdoc(doc.content, "default", [StarterKit])
      return Buffer.from(Y.encodeStateAsUpdate(ydoc))
    }
  } catch (err) {
    console.error("[onLoadDocument] parse", err)
  }
  return null
}

async function storeToUltid(context, document) {
  if (!context?.path || !context?.user) {
    throw new Error("store missing path or user in context")
  }
  const state = Buffer.from(Y.encodeStateAsUpdate(document)).toString("base64")
  let tiptapJson = null
  try {
    tiptapJson = TiptapTransformer.fromYdoc(document, "default")
  } catch {
    /* optional */
  }
  const body = {
    room: context.room ?? context.path,
    path: context.path,
    user: context.user,
    sub: context.sub ?? "",
    yjsState: state,
    document: tiptapJson,
  }
  const res = await fetch(`${ULTID_URL}/api/v1/richtext/hooks/store`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(SECRET ? { "X-Hocuspocus-Secret": SECRET } : {}),
    },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const detail = await res.text().catch(() => "")
    throw new Error(`store failed: ${res.status}${detail ? ` ${detail}` : ""}`)
  }
}

const server = new Server({
  port: PORT,
  debounce: 3000,
  maxDebounce: 10000,

  async onAuthenticate(data) {
    const { token, documentName } = data
    if (!SECRET) return {}
    const payload = decodeJwtPayload(token)
    if (!payload?.room || payload.room !== documentName) {
      console.error("[onAuthenticate] forbidden", { documentName, room: payload?.room })
      throw new Error("forbidden")
    }
    if (payload.exp && payload.exp * 1000 < Date.now()) {
      console.error("[onAuthenticate] token expired", { documentName })
      throw new Error("token expired")
    }
    return {
      room: payload.room,
      path: payload.path,
      user: payload.user,
      sub: payload.sub,
      name: payload.name,
      mode: payload.mode,
    }
  },

  async onLoadDocument(data) {
    const ctx = hookContext(data)
    return await loadFromUltid(ctx)
  },

  async onStoreDocument(data) {
    const ctx = hookContext(data)
    if (ctx.mode === "view") return
    try {
      await storeToUltid(ctx, data.document)
    } catch (err) {
      console.error("[onStoreDocument]", err)
    }
  },
})

server.listen()
console.log(`Hocuspocus listening on :${PORT}`)
