const pull = require("../server/node_modules/pull-stream")
const crypto = require("crypto")
const fs = require("fs")
const { buildValidatedTombstoneSet } = require('./tombstone_validator')
const path = require("path")
const { getConfig } = require("../configs/config-manager.js")
const logLimit = getConfig().ssbLogStream?.limit || 1000

const safeText = (v) => String(v || "").trim()
const normalizeTags = (raw) => {
  if (!raw) return []
  if (Array.isArray(raw)) return raw.map(t => String(t || "").trim()).filter(Boolean)
  return String(raw).split(",").map(t => t.trim()).filter(Boolean)
}
const INVITE_SALT = "SolarNET.HuB-pads"
const INVITE_BYTES = 16
const MEMBER_COLORS = ["#e74c3c","#3498db","#2ecc71","#f39c12","#9b59b6","#1abc9c","#e67e22","#e91e63","#00bcd4","#8bc34a"]

module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) => {
  let ssb
  const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }

  const ownCrypto = padCrypto || tribeCrypto
  const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null
  const lookupKeys = (rid) => {
    const a = (ownCrypto && ownCrypto.getKeys(rid)) || []
    if (a.length) return a
    return (tribeCrypto && tribeCrypto.getKeys(rid)) || []
  }
  const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0)

  let keyringPath = null
  let migratedToTribeCrypto = false
  const getLegacyKeyringPath = () => {
    if (!keyringPath) {
      const ssbConfig = require("../server/node_modules/ssb-config/inject")()
      keyringPath = path.join(ssbConfig.path, "pad-keys.json")
    }
    return keyringPath
  }
  const migrateLegacyKeyring = () => {
    if (migratedToTribeCrypto || !ownCrypto) { migratedToTribeCrypto = true; return }
    migratedToTribeCrypto = true
    try {
      const p = getLegacyKeyringPath()
      if (!fs.existsSync(p)) return
      const legacy = JSON.parse(fs.readFileSync(p, "utf8")) || {}
      for (const [rootId, keyHex] of Object.entries(legacy)) {
        if (rootId && keyHex && !ownCrypto.getKey(rootId)) {
          ownCrypto.setKey(rootId, keyHex, 1)
        }
      }
    } catch (_) {}
  }
  const getPadKey = (rootId) => {
    migrateLegacyKeyring()
    if (ownCrypto) return lookupKey(rootId)
    try { return JSON.parse(fs.readFileSync(getLegacyKeyringPath(), "utf8"))[rootId] || null } catch (_) { return null }
  }
  const setPadKey = (rootId, keyHex) => {
    migrateLegacyKeyring()
    if (ownCrypto) { ownCrypto.setKey(rootId, keyHex, 1); return }
    let kr = {}
    try { kr = JSON.parse(fs.readFileSync(getLegacyKeyringPath(), "utf8")) } catch (_) {}
    kr[rootId] = keyHex
    fs.writeFileSync(getLegacyKeyringPath(), JSON.stringify(kr, null, 2), "utf8")
  }

  const encryptField = (text, keyHex) => {
    const key = Buffer.from(keyHex, "hex")
    const iv = crypto.randomBytes(12)
    const cipher = crypto.createCipheriv("aes-256-gcm", key, iv)
    const enc = Buffer.concat([cipher.update(text, "utf8"), cipher.final()])
    const authTag = cipher.getAuthTag()
    return iv.toString("hex") + authTag.toString("hex") + enc.toString("hex")
  }

  const decryptField = (encrypted, keyHex) => {
    try {
      const key = Buffer.from(keyHex, "hex")
      const iv = Buffer.from(encrypted.slice(0, 24), "hex")
      const authTag = Buffer.from(encrypted.slice(24, 56), "hex")
      const ciphertext = Buffer.from(encrypted.slice(56), "hex")
      const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv)
      decipher.setAuthTag(authTag)
      return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8")
    } catch (_) { return "" }
  }

  const tryDecryptField = (encrypted, keyHex) => {
    const key = Buffer.from(keyHex, "hex")
    const iv = Buffer.from(encrypted.slice(0, 24), "hex")
    const authTag = Buffer.from(encrypted.slice(24, 56), "hex")
    const ciphertext = Buffer.from(encrypted.slice(56), "hex")
    const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv)
    decipher.setAuthTag(authTag)
    return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8")
  }

  const getTribeKeysFor = async (tribeId) => {
    if (!tribeCrypto || !tribesModel || !tribeId) return []
    try {
      const rootId = await tribesModel.getRootId(tribeId)
      const keys = tribeCrypto.getKeys(rootId) || []
      return keys
    } catch (_) { return [] }
  }

  const decryptWithKeys = (c, keys) => {
    if (!c.title || !keys.length) return null
    for (const k of keys) {
      try {
        const title = tryDecryptField(c.title, k)
        let deadline = ""
        let tagsRaw = ""
        try { deadline = c.deadline ? tryDecryptField(c.deadline, k) : "" } catch (_) {}
        try { tagsRaw = c.tags ? tryDecryptField(c.tags, k) : "" } catch (_) {}
        return { title: safeText(title), deadline, tags: normalizeTags(tagsRaw) }
      } catch (_) {}
    }
    return null
  }

  const encryptForInvite = (padKeyHex, code, saltHex) => {
    const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT)
    const derived = crypto.scryptSync(code, salt, 32)
    return encryptField(padKeyHex, derived.toString("hex"))
  }

  const decryptFromInvite = (encryptedKey, code, saltHex) => {
    const salt = saltHex ? Buffer.from(saltHex, "hex") : Buffer.from(INVITE_SALT)
    const derived = crypto.scryptSync(code, salt, 32)
    return decryptField(encryptedKey, derived.toString("hex"))
  }

  const tryDecryptPublicInviteKey = (invites) => {
    if (!Array.isArray(invites)) return null
    for (const inv of invites) {
      if (!inv || typeof inv !== "object") continue
      if (inv.public !== true) continue
      if (typeof inv.code !== "string" || typeof inv.ek !== "string") continue
      try {
        const key = decryptFromInvite(inv.ek, inv.code, inv.salt)
        if (key) return key
      } catch (_) {}
    }
    return null
  }

  const generateInviteSalt = () => crypto.randomBytes(16).toString("hex")

  const rotatePadKey = async (rootId, remainingMembers) => {
    if (!ownCrypto || !tribeCrypto || !rootId) return
    const existing = getPadKey(rootId)
    if (!existing) return
    const newKey = crypto.randomBytes(32).toString("hex")
    const newGen = ownCrypto.addNewKey(rootId, newKey)
    if (!Array.isArray(remainingMembers) || !remainingMembers.length) return
    const ssbClient = await openSsb()
    const ssbKeys = require("../server/node_modules/ssb-keys")
    const memberKeys = {}
    for (const m of remainingMembers) {
      try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {}
    }
    if (Object.keys(memberKeys).length) {
      await new Promise((resolve) => {
        ssbClient.publish({ type: "tribe-keys", tribeId: rootId, generation: newGen, memberKeys }, () => resolve())
      })
    }
  }

  const ingestOwnTribeKeys = async () => {
    if (!ownCrypto) return
    try {
      const ssbClient = await openSsb()
      const ssbKeys = require("../server/node_modules/ssb-keys")
      const config = require("../server/ssb_config")
      const msgs = await readAll(ssbClient)
      for (const m of msgs) {
        const c = m.value && m.value.content
        if (!c || c.type !== "tribe-keys") continue
        const memberKeys = c.memberKeys
        if (!memberKeys || typeof memberKeys !== "object") continue
        const boxed = memberKeys[ssbClient.id]
        if (!boxed) continue
        try {
          const unboxed = ssbKeys.unbox(boxed, config.keys)
          const key = typeof unboxed === "string" ? unboxed : (unboxed && unboxed.toString ? unboxed.toString() : null)
          if (key && c.tribeId) ownCrypto.addNewKey(c.tribeId, key)
        } catch (_) {}
      }
    } catch (_) {}
  }

  const readAll = async (ssbClient) =>
    new Promise((resolve, reject) =>
      pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs)))
    )

  const buildIndex = (messages) => {
    const tomb = new Set()
    const nodes = new Map()
    const parent = new Map()
    const child = new Map()
    const authorByKey = new Map()
    const tombRequests = []

    for (const m of messages) {
      const k = m.key
      const v = m.value || {}
      const c = v.content
      if (!c) continue
      if (c.type === "tombstone" && c.target) { tombRequests.push({ target: c.target, author: v.author }); continue }
      if (c.type === "pad") {
        nodes.set(k, { key: k, ts: v.timestamp || m.timestamp || 0, c, author: v.author })
        authorByKey.set(k, v.author)
        if (c.replaces) { parent.set(k, c.replaces); child.set(c.replaces, k) }
      }
    }

    for (const t of tombRequests) {
      const targetAuthor = authorByKey.get(t.target)
      if (targetAuthor && t.author === targetAuthor) tomb.add(t.target)
    }

    const rootOf = (id) => { let cur = id; while (parent.has(cur)) cur = parent.get(cur); return cur }
    const tipOf = (id) => { let cur = id; while (child.has(cur)) cur = child.get(cur); return cur }

    const roots = new Set()
    for (const id of nodes.keys()) roots.add(rootOf(id))
    const tipByRoot = new Map()
    for (const r of roots) tipByRoot.set(r, tipOf(r))

    return { tomb, nodes, parent, child, rootOf, tipOf, tipByRoot }
  }

  const decryptPadFields = (c, rootId, tribeKeys) => {
    if (c.encrypted !== true) {
      return { title: safeText(c.title), deadline: c.deadline ? String(c.deadline) : "", tags: normalizeTags(c.tags) }
    }
    if (c.tribeId && Array.isArray(tribeKeys) && tribeKeys.length) {
      const viaTribe = decryptWithKeys(c, tribeKeys)
      if (viaTribe) return viaTribe
    }
    let keyHex = getPadKey(rootId)
    if (!keyHex) keyHex = tryDecryptPublicInviteKey(c.invites)
    if (!keyHex) return { title: "", deadline: "", tags: [] }
    const title = c.title ? decryptField(c.title, keyHex) : ""
    const deadline = c.deadline ? decryptField(c.deadline, keyHex) : ""
    const tagsRaw = c.tags ? decryptField(c.tags, keyHex) : ""
    const tags = normalizeTags(tagsRaw)
    return { title, deadline, tags }
  }

  const buildPad = (node, rootId, tribeKeys) => {
    const c = node.c || {}
    if (c.type !== "pad") return null
    const { title, deadline, tags } = decryptPadFields(c, rootId, tribeKeys)
    return {
      key: node.key,
      rootId,
      title,
      status: c.status || "OPEN",
      deadline,
      tags,
      author: c.author || node.author,
      members: Array.isArray(c.members) ? c.members : [],
      invites: Array.isArray(c.invites) ? c.invites : [],
      createdAt: c.createdAt || new Date(node.ts).toISOString(),
      updatedAt: c.updatedAt || null,
      tribeId: c.tribeId || null,
      encrypted: c.encrypted === true
    }
  }

  const isClosed = (pad) => {
    if (pad.status === "CLOSED") return true
    if (!pad.deadline) return false
    return new Date(pad.deadline).getTime() <= Date.now()
  }

  return {
    type: "pad",

    async decryptContent(content, rootId) {
      const tKeys = content && content.tribeId ? await getTribeKeysFor(content.tribeId) : []
      return decryptPadFields(content, rootId, tKeys)
    },

    decryptContentPublicSync(content) {
      if (!content) return null
      if (content.encrypted !== true) {
        return { title: safeText(content.title), deadline: content.deadline ? String(content.deadline) : "", tags: normalizeTags(content.tags) }
      }
      if (content.tribeId) return null
      const keyHex = tryDecryptPublicInviteKey(content.invites)
      if (!keyHex) return null
      try {
        const title = content.title ? decryptField(content.title, keyHex) : ""
        const deadline = content.deadline ? decryptField(content.deadline, keyHex) : ""
        const tagsRaw = content.tags ? decryptField(content.tags, keyHex) : ""
        return { title, deadline, tags: normalizeTags(tagsRaw) }
      } catch (_) { return null }
    },

    async resolveRootId(id) {
      const ssbClient = await openSsb()
      const messages = await readAll(ssbClient)
      const idx = buildIndex(messages)
      let tip = id
      while (idx.child.has(tip)) tip = idx.child.get(tip)
      if (idx.tomb.has(tip)) throw new Error("Not found")
      let root = tip
      while (idx.parent.has(root)) root = idx.parent.get(root)
      return root
    },

    async resolveCurrentId(id) {
      const ssbClient = await openSsb()
      const messages = await readAll(ssbClient)
      const idx = buildIndex(messages)
      let tip = id
      while (idx.child.has(tip)) tip = idx.child.get(tip)
      if (idx.tomb.has(tip)) throw new Error("Not found")
      return tip
    },

    async createPad(title, status, deadline, tagsRaw, tribeId) {
      const ssbClient = await openSsb()
      const now = new Date().toISOString()
      const validStatus = ["OPEN", "INVITE-ONLY"].includes(String(status).toUpperCase()) ? String(status).toUpperCase() : "OPEN"
      const userId = ssbClient.id
      const tagsArr = normalizeTags(tagsRaw)

      const isPublicOpen = validStatus === "OPEN" && !tribeId
      if (isPublicOpen) {
        const content = {
          type: "pad",
          title: safeText(title),
          status: validStatus,
          deadline: deadline ? String(deadline) : "",
          tags: tagsArr,
          author: userId,
          members: [userId],
          invites: [],
          createdAt: now,
          updatedAt: now,
          encrypted: false
        }
        return new Promise((resolve, reject) => {
          ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
        })
      }

      let keyHex = null
      let usesTribeKey = false
      if (tribeId) {
        const tKeys = await getTribeKeysFor(tribeId)
        if (tKeys.length) { keyHex = tKeys[0]; usesTribeKey = true }
      }
      if (!keyHex) keyHex = crypto.randomBytes(32).toString("hex")
      const enc = (text) => encryptField(text, keyHex)

      const content = {
        type: "pad",
        title: enc(safeText(title)),
        status: validStatus,
        deadline: deadline ? enc(String(deadline)) : "",
        tags: enc(tagsArr.join(",")),
        author: userId,
        members: [userId],
        invites: [],
        createdAt: now,
        updatedAt: now,
        encrypted: true,
        ...(tribeId ? { tribeId } : {})
      }

      return new Promise((resolve, reject) => {
        ssbClient.publish(content, (err, msg) => {
          if (err) return reject(err)
          if (!usesTribeKey) {
            setPadKey(msg.key, keyHex)
            if (tribeCrypto) {
              try {
                const ssbKeys = require("../server/node_modules/ssb-keys")
                const boxedKey = tribeCrypto.boxKeyForMember(keyHex, userId, ssbKeys)
                ssbClient.publish({ type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve(msg))
                return
              } catch (_) {}
            }
          }
          resolve(msg)
        })
      })
    },

    async updatePadById(id, data) {
      const tipId = await this.resolveCurrentId(id)
      const ssbClient = await openSsb()
      const userId = ssbClient.id
      const rootId = await this.resolveRootId(id)

      return new Promise(async (resolve, reject) => {
        ssbClient.get(tipId, async (err, item) => {
          if (err || !item?.content) return reject(new Error("Pad not found"))
          if (item.content.author !== userId) return reject(new Error("Not the author"))
          const c = item.content
          const isEncrypted = c.encrypted === true
          let keyHex = null
          let usesTribeKey = false
          if (isEncrypted) {
            if (c.tribeId) {
              const tKeys = await getTribeKeysFor(c.tribeId)
              if (tKeys.length) { keyHex = tKeys[0]; usesTribeKey = true }
            }
            if (!keyHex) keyHex = getPadKey(rootId)
            if (!keyHex) return reject(new Error(`Missing pad key for ${rootId} — cannot update pad`))
          }
          const enc = (text) => isEncrypted ? encryptField(text, keyHex) : text
          const tagsField = (raw) => isEncrypted ? encryptField(normalizeTags(raw).join(","), keyHex) : normalizeTags(raw)
          const updated = {
            ...c,
            title: data.title !== undefined ? enc(safeText(data.title)) : c.title,
            status: data.status !== undefined ? (["OPEN","INVITE-ONLY"].includes(String(data.status).toUpperCase()) ? String(data.status).toUpperCase() : c.status) : c.status,
            deadline: data.deadline !== undefined ? (isEncrypted ? enc(String(data.deadline)) : String(data.deadline)) : c.deadline,
            tags: data.tags !== undefined ? tagsField(data.tags) : c.tags,
            updatedAt: new Date().toISOString(),
            replaces: tipId
          }
          const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
          ssbClient.publish(tombstone, (e1) => {
            if (e1) return reject(e1)
            ssbClient.publish(updated, (e2, res) => {
              if (e2) return reject(e2)
              if (keyHex && !usesTribeKey) setPadKey(res.key, keyHex)
              resolve(res)
            })
          })
        })
      })
    },

    async closePadById(id) {
      const tipId = await this.resolveCurrentId(id)
      const ssbClient = await openSsb()
      const userId = ssbClient.id
      const rootId = await this.resolveRootId(id)
      return new Promise(async (resolve, reject) => {
        ssbClient.get(tipId, async (err, item) => {
          if (err || !item?.content) return reject(new Error("Pad not found"))
          if (item.content.author !== userId) return reject(new Error("Not the author"))
          const c = item.content
          let keyHex = null
          let usesTribeKey = false
          if (c.tribeId) {
            const tKeys = await getTribeKeysFor(c.tribeId)
            if (tKeys.length) { keyHex = tKeys[0]; usesTribeKey = true }
          }
          if (!keyHex) keyHex = getPadKey(rootId)
          const updated = {
            ...c,
            status: "CLOSED",
            updatedAt: new Date().toISOString(),
            replaces: tipId
          }
          const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
          ssbClient.publish(tombstone, (e1) => {
            if (e1) return reject(e1)
            ssbClient.publish(updated, (e2, res) => {
              if (e2) return reject(e2)
              if (keyHex && !usesTribeKey) setPadKey(res.key, keyHex)
              resolve(res)
            })
          })
        })
      })
    },

    async leavePad(padId) {
      const ssbClient = await openSsb()
      const userId = ssbClient.id
      const pad = await this.getPadById(padId)
      if (!pad) throw new Error("Pad not found")
      if (pad.author === userId) throw new Error("Author cannot leave their own pad")
      const members = (Array.isArray(pad.members) ? pad.members : []).filter(m => m !== userId)
      if (!Array.isArray(pad.members) || !pad.members.includes(userId)) return
      const tipId = await this.resolveCurrentId(padId)
      const rootId = await this.resolveRootId(padId)
      await new Promise((resolve, reject) => {
        ssbClient.get(tipId, (err, item) => {
          if (err || !item?.content) return reject(new Error("Pad not found"))
          const updated = { ...item.content, members, updatedAt: new Date().toISOString(), replaces: tipId }
          const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
          ssbClient.publish(tombstone, (e1) => {
            if (e1) return reject(e1)
            ssbClient.publish(updated, (e2) => e2 ? reject(e2) : resolve())
          })
        })
      })
      try { await rotatePadKey(rootId, members) } catch (_) {}
    },

    async ingestKeys() { await ingestOwnTribeKeys() },

    async pruneOrphanKeys() {
      if (!ownCrypto || typeof ownCrypto.getAllRootIds !== "function") return 0
      try {
        const ssbClient = await openSsb()
        const messages = await readAll(ssbClient)
        const live = new Set()
        const tomb = buildValidatedTombstoneSet(messages)
        for (const m of messages) {
          const c = m.value && m.value.content
          if (!c) continue
          if (c.type === "pad") live.add(m.key)
        }
        const all = ownCrypto.getAllRootIds()
        let removed = 0
        for (const rid of all) {
          if (!live.has(rid) || tomb.has(rid)) {
            try { ownCrypto.dropKey(rid); removed += 1 } catch (_) {}
          }
        }
        return removed
      } catch (_) { return 0 }
    },

    async addMemberToPad(padId, feedId) {
      const tipId = await this.resolveCurrentId(padId)
      const ssbClient = await openSsb()
      const rootId = await this.resolveRootId(padId)

      return new Promise((resolve, reject) => {
        ssbClient.get(tipId, (err, item) => {
          if (err || !item?.content) return reject(new Error("Pad not found"))
          const c = item.content
          const members = Array.isArray(c.members) ? c.members : []
          if (members.includes(feedId)) return resolve()
          if (c.encrypted === true && !c.tribeId && !getPadKey(rootId)) {
            const key = tryDecryptPublicInviteKey(c.invites)
            if (key) setPadKey(rootId, key)
          }
          const updated = { ...c, members: [...members, feedId], updatedAt: new Date().toISOString(), replaces: tipId }
          const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: ssbClient.id }
          ssbClient.publish(tombstone, (e1) => {
            if (e1) return reject(e1)
            ssbClient.publish(updated, (e2, res) => {
              if (e2) return reject(e2)
              if (c.encrypted === true && !c.tribeId) {
                const keyHex = getPadKey(rootId)
                if (keyHex) setPadKey(res.key, keyHex)
              }
              resolve(res)
            })
          })
        })
      })
    },

    async deletePadById(id) {
      const tipId = await this.resolveCurrentId(id)
      const ssbClient = await openSsb()
      const userId = ssbClient.id
      return new Promise((resolve, reject) => {
        ssbClient.get(tipId, (err, item) => {
          if (err || !item?.content) return reject(new Error("Pad not found"))
          if (item.content.author !== userId) return reject(new Error("Not the author"))
          const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
          ssbClient.publish(tombstone, (e) => e ? reject(e) : resolve())
        })
      })
    },

    async getPadById(id) {
      const ssbClient = await openSsb()
      const messages = await readAll(ssbClient)
      const idx = buildIndex(messages)
      let tip = id
      while (idx.child.has(tip)) tip = idx.child.get(tip)
      if (idx.tomb.has(tip)) return null
      const node = idx.nodes.get(tip)
      if (!node || node.c.type !== "pad") return null
      let root = tip
      while (idx.parent.has(root)) root = idx.parent.get(root)
      const tKeys = node.c.tribeId ? await getTribeKeysFor(node.c.tribeId) : []
      const pad = buildPad(node, root, tKeys)
      if (!pad) return null
      pad.isClosed = isClosed(pad)
      return pad
    },

    async listAll({ filter = "all", viewerId } = {}) {
      const ssbClient = await openSsb()
      const uid = viewerId || ssbClient.id
      const messages = await readAll(ssbClient)
      const idx = buildIndex(messages)
      const tribeKeyCache = new Map()
      const items = []
      for (const [rootId, tipId] of idx.tipByRoot.entries()) {
        if (idx.tomb.has(tipId)) continue
        const node = idx.nodes.get(tipId)
        if (!node || node.c.type !== "pad") continue
        let tKeys = []
        if (node.c.tribeId) {
          if (!tribeKeyCache.has(node.c.tribeId)) {
            tribeKeyCache.set(node.c.tribeId, await getTribeKeysFor(node.c.tribeId))
          }
          tKeys = tribeKeyCache.get(node.c.tribeId)
        }
        const pad = buildPad(node, rootId, tKeys)
        if (!pad) continue
        pad.isClosed = isClosed(pad)
        items.push(pad)
      }
      const now = Date.now()
      let list = items
      if (filter === "mine") list = list.filter(p => p.author === uid)
      else if (filter === "recent") list = list.filter(p => new Date(p.createdAt).getTime() >= now - 86400000)
      else if (filter === "open") list = list.filter(p => !p.isClosed)
      else if (filter === "closed") list = list.filter(p => p.isClosed)
      return list.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
    },

    async generateInvite(padId) {
      const ssbClient = await openSsb()
      const userId = ssbClient.id
      const pad = await this.getPadById(padId)
      if (!pad) throw new Error("Pad not found")
      if (pad.author !== userId) throw new Error("Only the author can generate invites")
      const rootId = await this.resolveRootId(padId)
      const keyHex = getPadKey(rootId)
      const code = crypto.randomBytes(INVITE_BYTES).toString("hex")
      let invite = code
      if (keyHex) {
        const inviteSalt = generateInviteSalt()
        const ek = encryptForInvite(keyHex, code, inviteSalt)
        invite = { code, ek, salt: inviteSalt }
      }
      const invites = [...pad.invites, invite]
      await this.updatePadById(padId, { invites })
      return code
    },

    async joinByInvite(code) {
      const ssbClient = await openSsb()
      const userId = ssbClient.id
      const pads = await this.listAll()
      let matchedPad = null
      let matchedInvite = null
      for (const p of pads) {
        for (const inv of p.invites) {
          if (typeof inv === "string" && inv === code) { matchedPad = p; matchedInvite = inv; break }
          if (typeof inv === "object" && inv.code === code) { matchedPad = p; matchedInvite = inv; break }
        }
        if (matchedPad) break
      }
      if (!matchedPad) throw new Error("Invalid or expired invite code")
      if (matchedPad.members.includes(userId)) throw new Error("Already a member")
      let padKey = null
      let resolvedRootId = null
      if (typeof matchedInvite === "object" && matchedInvite.ek) {
        padKey = decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt)
        resolvedRootId = await this.resolveRootId(matchedPad.rootId)
        setPadKey(resolvedRootId, padKey)
      }
      await this.addMemberToPad(matchedPad.rootId, userId)
      if (tribeCrypto && padKey && resolvedRootId) {
        try {
          const ssbKeys = require("../server/node_modules/ssb-keys")
          const memberKeys = {}
          try { memberKeys[userId] = tribeCrypto.boxKeyForMember(padKey, userId, ssbKeys) } catch (_) {}
          if (matchedPad.author && matchedPad.author !== userId) {
            try { memberKeys[matchedPad.author] = tribeCrypto.boxKeyForMember(padKey, matchedPad.author, ssbKeys) } catch (_) {}
          }
          if (Object.keys(memberKeys).length) {
            await new Promise((resolve) => {
              ssbClient.publish({ type: "tribe-keys", tribeId: resolvedRootId, generation: 1, memberKeys }, () => resolve())
            })
          }
        } catch (_) {}
      }
      const isPublicInvite = typeof matchedInvite === "object" && matchedInvite.public === true
      const invites = isPublicInvite ? matchedPad.invites : matchedPad.invites.filter(inv => {
        if (typeof inv === "string") return inv !== code
        return inv.code !== code
      })
      const tipId = await this.resolveCurrentId(matchedPad.rootId)
      const ssbC = await openSsb()
      return new Promise((resolve, reject) => {
        ssbC.get(tipId, (err, item) => {
          if (err || !item?.content) return reject(new Error("Pad not found after join"))
          const updated = { ...item.content, invites, updatedAt: new Date().toISOString(), replaces: tipId }
          const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId }
          ssbC.publish(tombstone, (e1) => {
            if (e1) return reject(e1)
            ssbC.publish(updated, (e2, res) => e2 ? reject(e2) : resolve(matchedPad.rootId))
          })
        })
      })
    },

    async addEntry(padId, text) {
      const ssbClient = await openSsb()
      const rootId = await this.resolveRootId(padId)
      const pad = await this.getPadById(rootId)
      const padIsEncrypted = !!(pad && pad.encrypted)
      const now = new Date().toISOString()
      const safeBody = safeText(text)

      if (!padIsEncrypted) {
        const content = {
          type: "padEntry",
          padId: rootId,
          text: safeBody,
          author: ssbClient.id,
          createdAt: now,
          encrypted: false,
          ...(pad && pad.tribeId ? { tribeId: pad.tribeId } : {})
        }
        return new Promise((resolve, reject) => {
          ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
        })
      }

      let keyHex = null
      if (pad && pad.tribeId) {
        const tKeys = await getTribeKeysFor(pad.tribeId)
        if (tKeys.length) keyHex = tKeys[0]
      }
      if (!keyHex) keyHex = getPadKey(rootId)
      if (!keyHex) throw new Error(`Missing pad key for ${rootId} — cannot publish pad entry`)
      const content = {
        type: "padEntry",
        padId: rootId,
        text: encryptField(safeBody, keyHex),
        author: ssbClient.id,
        createdAt: now,
        encrypted: true,
        ...(pad && pad.tribeId ? { tribeId: pad.tribeId } : {})
      }
      return new Promise((resolve, reject) => {
        ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg))
      })
    },

    async getEntries(padRootId) {
      const ssbClient = await openSsb()
      const messages = await readAll(ssbClient)
      const pad = await this.getPadById(padRootId)
      const padKey = getPadKey(padRootId)
      let tribeKeys = []
      if (pad && pad.tribeId) {
        tribeKeys = await getTribeKeysFor(pad.tribeId)
      }
      const entries = []
      for (const m of messages) {
        const v = m.value || {}
        const c = v.content
        if (!c || c.type !== "padEntry") continue
        if (c.padId !== padRootId) continue
        let text = c.text || ""
        if (c.encrypted && c.text) {
          let decoded = ""
          for (const k of tribeKeys) {
            try { decoded = tryDecryptField(c.text, k); break } catch (_) {}
          }
          if (!decoded && padKey) decoded = decryptField(c.text, padKey)
          text = decoded
        }
        entries.push({
          key: m.key,
          author: c.author || v.author,
          text,
          createdAt: c.createdAt || new Date(v.timestamp || 0).toISOString()
        })
      }
      entries.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
      return entries
    },

    getMemberColor(members, feedId) {
      const idx = members.indexOf(feedId)
      return idx >= 0 ? MEMBER_COLORS[idx % MEMBER_COLORS.length] : "#888"
    }
  }
}
